Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-01-25 03:11:15 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-01-25 03:11:15 +0300
commitce545ca5d74c9eabf6f2412c1332820c81342271 (patch)
tree33063c2e1b75b22797cd837623e19ca1c0bddd59
parentba0d8b409534f02c23bf2447fd32246926ba4392 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/graphql_shared/utils.js12
-rw-r--r--app/controllers/jira_connect/public_keys_controller.rb4
-rw-r--r--app/controllers/jira_connect/subscriptions_controller.rb1
-rw-r--r--app/models/concerns/web_hooks/auto_disabling.rb69
-rw-r--r--app/models/concerns/web_hooks/unstoppable.rb29
-rw-r--r--app/models/hooks/project_hook.rb1
-rw-r--r--app/models/hooks/service_hook.rb7
-rw-r--r--app/models/hooks/system_hook.rb1
-rw-r--r--app/models/hooks/web_hook.rb41
-rw-r--r--app/models/jira_connect_installation.rb2
-rw-r--r--config/feature_flags/development/jira_connect_oauth_self_managed.yml8
-rw-r--r--doc/api/container_registry.md4
-rw-r--r--doc/integration/jira/development_panel.md2
-rw-r--r--doc/user/application_security/dast/browser_based.md4
-rw-r--r--doc/user/application_security/dast/dast_troubleshooting.md2
-rw-r--r--doc/user/application_security/dast/run_dast_offline.md4
-rw-r--r--doc/user/application_security/dependency_scanning/index.md20
-rw-r--r--doc/user/application_security/policies/scan-execution-policies.md14
-rw-r--r--doc/user/clusters/environments.md2
-rw-r--r--spec/frontend/graphql_shared/utils_spec.js26
-rw-r--r--spec/helpers/jira_connect_helper_spec.rb13
-rw-r--r--spec/models/concerns/triggerable_hooks_spec.rb2
-rw-r--r--spec/models/hooks/project_hook_spec.rb14
-rw-r--r--spec/models/hooks/service_hook_spec.rb38
-rw-r--r--spec/models/hooks/system_hook_spec.rb12
-rw-r--r--spec/models/hooks/web_hook_spec.rb400
-rw-r--r--spec/models/jira_connect_installation_spec.rb8
-rw-r--r--spec/models/project_spec.rb12
-rw-r--r--spec/requests/jira_connect/public_keys_controller_spec.rb12
-rw-r--r--spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb272
-rw-r--r--spec/support/shared_examples/models/concerns/unstoppable_hooks_shared_examples.rb177
-rw-r--r--spec/support/shared_examples/requests/api/hooks_shared_examples.rb14
32 files changed, 762 insertions, 465 deletions
diff --git a/app/assets/javascripts/graphql_shared/utils.js b/app/assets/javascripts/graphql_shared/utils.js
index 8fb70eb59bd..806e89d6e9f 100644
--- a/app/assets/javascripts/graphql_shared/utils.js
+++ b/app/assets/javascripts/graphql_shared/utils.js
@@ -104,3 +104,15 @@ export const convertNodeIdsFromGraphQLIds = (nodes) => {
return nodes.map((node) => (node.id ? { ...node, id: getIdFromGraphQLId(node.id) } : node));
};
+
+/**
+ * This function takes a GraphQL query data as a required argument and
+ * the field name to resolve as an optional argument
+ * and returns resolved field's data or an empty array
+ * @param {Object} queryData
+ * @param {String} nodesField (in most cases it will be 'nodes')
+ * @returns {Array}
+ */
+export const getNodesOrDefault = (queryData, nodesField = 'nodes') => {
+ return queryData?.[nodesField] ?? [];
+};
diff --git a/app/controllers/jira_connect/public_keys_controller.rb b/app/controllers/jira_connect/public_keys_controller.rb
index 09003f8478f..2d767cda699 100644
--- a/app/controllers/jira_connect/public_keys_controller.rb
+++ b/app/controllers/jira_connect/public_keys_controller.rb
@@ -10,9 +10,7 @@ module JiraConnect
skip_before_action :authenticate_user!
def show
- if Feature.disabled?(:jira_connect_oauth_self_managed) || !Gitlab.config.jira_connect.enable_public_keys_storage
- return render_404
- end
+ return render_404 unless Gitlab.config.jira_connect.enable_public_keys_storage
render plain: public_key.key
end
diff --git a/app/controllers/jira_connect/subscriptions_controller.rb b/app/controllers/jira_connect/subscriptions_controller.rb
index ff7477a94d6..a206e7fbbd8 100644
--- a/app/controllers/jira_connect/subscriptions_controller.rb
+++ b/app/controllers/jira_connect/subscriptions_controller.rb
@@ -21,7 +21,6 @@ class JiraConnect::SubscriptionsController < JiraConnect::ApplicationController
before_action do
push_frontend_feature_flag(:jira_connect_oauth, @user)
- push_frontend_feature_flag(:jira_connect_oauth_self_managed, @user)
end
before_action :allow_rendering_in_iframe, only: :index
diff --git a/app/models/concerns/web_hooks/auto_disabling.rb b/app/models/concerns/web_hooks/auto_disabling.rb
new file mode 100644
index 00000000000..2cc17a6f185
--- /dev/null
+++ b/app/models/concerns/web_hooks/auto_disabling.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+module WebHooks
+ module AutoDisabling
+ extend ActiveSupport::Concern
+
+ included do
+ # A hook is disabled if:
+ #
+ # - we are no longer in the grace-perod (recent_failures > ?)
+ # - and either:
+ # - disabled_until is nil (i.e. this was set by WebHook#fail!)
+ # - or disabled_until is in the future (i.e. this was set by WebHook#backoff!)
+ scope :disabled, -> do
+ where('recent_failures > ? AND (disabled_until IS NULL OR disabled_until >= ?)',
+ WebHook::FAILURE_THRESHOLD, Time.current)
+ end
+
+ # A hook is executable if:
+ #
+ # - we are still in the grace-period (recent_failures <= ?)
+ # - OR we have exceeded the grace period and neither of the following is true:
+ # - disabled_until is nil (i.e. this was set by WebHook#fail!)
+ # - disabled_until is in the future (i.e. this was set by WebHook#backoff!)
+ scope :executable, -> do
+ where('recent_failures <= ? OR (recent_failures > ? AND (disabled_until IS NOT NULL) AND (disabled_until < ?))',
+ WebHook::FAILURE_THRESHOLD, WebHook::FAILURE_THRESHOLD, Time.current)
+ end
+ end
+
+ def executable?
+ !temporarily_disabled? && !permanently_disabled?
+ end
+
+ def temporarily_disabled?
+ return false if recent_failures <= WebHook::FAILURE_THRESHOLD
+
+ disabled_until.present? && disabled_until >= Time.current
+ end
+
+ def permanently_disabled?
+ return false if disabled_until.present?
+
+ recent_failures > WebHook::FAILURE_THRESHOLD
+ end
+
+ def disable!
+ return if permanently_disabled?
+
+ super
+ end
+
+ def backoff!
+ return if permanently_disabled? || (backoff_count >= WebHook::MAX_FAILURES && temporarily_disabled?)
+
+ super
+ end
+
+ def alert_status
+ if temporarily_disabled?
+ :temporarily_disabled
+ elsif permanently_disabled?
+ :disabled
+ else
+ :executable
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/web_hooks/unstoppable.rb b/app/models/concerns/web_hooks/unstoppable.rb
new file mode 100644
index 00000000000..26284fe3c36
--- /dev/null
+++ b/app/models/concerns/web_hooks/unstoppable.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module WebHooks
+ module Unstoppable
+ extend ActiveSupport::Concern
+
+ included do
+ scope :executable, -> { all }
+
+ scope :disabled, -> { none }
+ end
+
+ def executable?
+ true
+ end
+
+ def temporarily_disabled?
+ false
+ end
+
+ def permanently_disabled?
+ false
+ end
+
+ def alert_status
+ :executable
+ end
+ end
+end
diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb
index dcba136d163..81122c3ea10 100644
--- a/app/models/hooks/project_hook.rb
+++ b/app/models/hooks/project_hook.rb
@@ -2,6 +2,7 @@
class ProjectHook < WebHook
include TriggerableHooks
+ include WebHooks::AutoDisabling
include Presentable
include Limitable
extend ::Gitlab::Utils::Override
diff --git a/app/models/hooks/service_hook.rb b/app/models/hooks/service_hook.rb
index 94ced96bbde..6af70c249a0 100644
--- a/app/models/hooks/service_hook.rb
+++ b/app/models/hooks/service_hook.rb
@@ -1,7 +1,9 @@
# frozen_string_literal: true
class ServiceHook < WebHook
+ include WebHooks::Unstoppable
include Presentable
+
extend ::Gitlab::Utils::Override
belongs_to :integration
@@ -13,9 +15,4 @@ class ServiceHook < WebHook
override :parent
delegate :parent, to: :integration
-
- override :executable?
- def executable?
- true
- end
end
diff --git a/app/models/hooks/system_hook.rb b/app/models/hooks/system_hook.rb
index 3c7f0ef9ffc..eaffe83cab3 100644
--- a/app/models/hooks/system_hook.rb
+++ b/app/models/hooks/system_hook.rb
@@ -2,6 +2,7 @@
class SystemHook < WebHook
include TriggerableHooks
+ include WebHooks::Unstoppable
triggerable_hooks [
:repository_update_hooks,
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index 49418cda3ac..819152a38c8 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -56,31 +56,6 @@ class WebHook < ApplicationRecord
all_branches: 2
}, _prefix: true
- scope :executable, -> do
- where('recent_failures <= ? AND (disabled_until IS NULL OR disabled_until < ?)', FAILURE_THRESHOLD, Time.current)
- end
-
- # Inverse of executable
- scope :disabled, -> do
- where('recent_failures > ? OR disabled_until >= ?', FAILURE_THRESHOLD, Time.current)
- end
-
- def executable?
- !temporarily_disabled? && !permanently_disabled?
- end
-
- def temporarily_disabled?
- return false if recent_failures <= FAILURE_THRESHOLD
-
- disabled_until.present? && disabled_until >= Time.current
- end
-
- def permanently_disabled?
- return false if disabled_until.present?
-
- recent_failures > FAILURE_THRESHOLD
- end
-
# rubocop: disable CodeReuse/ServiceClass
def execute(data, hook_name, force: false)
# hook.executable? is checked in WebHookService#execute
@@ -112,8 +87,6 @@ class WebHook < ApplicationRecord
end
def disable!
- return if permanently_disabled?
-
update_attribute(:recent_failures, EXCEEDED_FAILURE_THRESHOLD)
end
@@ -127,8 +100,6 @@ class WebHook < ApplicationRecord
# Don't actually back-off until FAILURE_THRESHOLD failures have been seen
# we mark the grace-period using the recent_failures counter
def backoff!
- return if permanently_disabled? || (backoff_count >= MAX_FAILURES && temporarily_disabled?)
-
attrs = { recent_failures: next_failure_count }
if recent_failures >= FAILURE_THRESHOLD
@@ -137,7 +108,7 @@ class WebHook < ApplicationRecord
end
assign_attributes(attrs)
- save(validate: false)
+ save(validate: false) if changed?
end
def failed!
@@ -167,16 +138,6 @@ class WebHook < ApplicationRecord
{ related_class: type }
end
- def alert_status
- if temporarily_disabled?
- :temporarily_disabled
- elsif permanently_disabled?
- :disabled
- else
- :executable
- end
- end
-
# Exclude binary columns by default - they have no sensible JSON encoding
def serializable_hash(options = nil)
options = options.try(:dup) || {}
diff --git a/app/models/jira_connect_installation.rb b/app/models/jira_connect_installation.rb
index 0e88d1ceae9..f07f979a06d 100644
--- a/app/models/jira_connect_installation.rb
+++ b/app/models/jira_connect_installation.rb
@@ -31,7 +31,7 @@ class JiraConnectInstallation < ApplicationRecord
end
def oauth_authorization_url
- return Gitlab.config.gitlab.url if instance_url.blank? || Feature.disabled?(:jira_connect_oauth_self_managed)
+ return Gitlab.config.gitlab.url if instance_url.blank?
instance_url
end
diff --git a/config/feature_flags/development/jira_connect_oauth_self_managed.yml b/config/feature_flags/development/jira_connect_oauth_self_managed.yml
deleted file mode 100644
index 02c4b9fa398..00000000000
--- a/config/feature_flags/development/jira_connect_oauth_self_managed.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: jira_connect_oauth_self_managed
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85483
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/359940
-milestone: '15.0'
-type: development
-group: group::integrations
-default_enabled: true
diff --git a/doc/api/container_registry.md b/doc/api/container_registry.md
index 344137e4ab3..5fc4cb138a1 100644
--- a/doc/api/container_registry.md
+++ b/doc/api/container_registry.md
@@ -12,7 +12,7 @@ This is the API documentation of the [GitLab Container Registry](../user/package
When the `ci_job_token_scope` feature flag is enabled (it is **disabled by default**), you can use the below endpoints
from a CI/CD job, by passing the `$CI_JOB_TOKEN` variable as the `JOB-TOKEN` header.
-The job token will only have access to its own project.
+The job token only has access to its own project.
[GitLab administrators with access to the GitLab Rails console](../administration/feature_flags.md)
can opt to enable it.
@@ -375,7 +375,7 @@ WARNING:
The number of tags deleted by this API is limited on GitLab.com
because of the scale of the Container Registry there.
If your Container Registry has a large number of tags to delete,
-only some of them will be deleted, and you might need to call this API multiple times.
+only some of them are deleted, and you might need to call this API multiple times.
To schedule tags for automatic deletion, use a [cleanup policy](../user/packages/container_registry/reduce_container_registry_storage.md#cleanup-policy) instead.
Examples:
diff --git a/doc/integration/jira/development_panel.md b/doc/integration/jira/development_panel.md
index ed2180b56aa..8fee11f8509 100644
--- a/doc/integration/jira/development_panel.md
+++ b/doc/integration/jira/development_panel.md
@@ -69,7 +69,7 @@ To simplify administration, we recommend that a GitLab group maintainer or group
| Jira usage | GitLab.com customers need | GitLab self-managed customers need |
|------------|---------------------------|------------------------------------|
-| [Atlassian cloud](https://www.atlassian.com/migration/assess/why-cloud) | The [GitLab.com for Jira Cloud app](https://marketplace.atlassian.com/apps/1221011/gitlab-com-for-jira-cloud?hosting=cloud&tab=overview) installed from the [Atlassian Marketplace](https://marketplace.atlassian.com). This method offers real-time sync between GitLab.com and Jira. For more information, see [GitLab.com for Jira Cloud app](connect-app.md). | The GitLab.com for Jira Cloud app [by using a workaround](connect-app.md#install-the-gitlabcom-for-jira-cloud-app-manually). When the `jira_connect_oauth_self_managed` feature flag is enabled, you can install the app from the [Atlassian Marketplace](https://marketplace.atlassian.com/). For more information, see [Connect the GitLab.com for Jira Cloud app for self-managed instances](connect-app.md#connect-the-gitlabcom-for-jira-cloud-app-for-self-managed-instances). |
+| [Atlassian cloud](https://www.atlassian.com/migration/assess/why-cloud) | The [GitLab.com for Jira Cloud app](https://marketplace.atlassian.com/apps/1221011/gitlab-com-for-jira-cloud?hosting=cloud&tab=overview) from the [Atlassian Marketplace](https://marketplace.atlassian.com). This method offers real-time sync between GitLab.com and Jira. For more information, see [GitLab.com for Jira Cloud app](connect-app.md). | The GitLab.com for Jira Cloud app [installed manually](connect-app.md#install-the-gitlabcom-for-jira-cloud-app-manually). By default, you can install the app from the [Atlassian Marketplace](https://marketplace.atlassian.com/). For more information, see [Connect the GitLab.com for Jira Cloud app for self-managed instances](connect-app.md#connect-the-gitlabcom-for-jira-cloud-app-for-self-managed-instances). |
| Your own server | The [Jira DVCS (distributed version control system) connector](dvcs/index.md). This syncs data hourly. | The [Jira DVCS (distributed version control system) connector](dvcs/index.md). This syncs data hourly. |
Each GitLab project can be configured to connect to an entire Jira instance. That means after
diff --git a/doc/user/application_security/dast/browser_based.md b/doc/user/application_security/dast/browser_based.md
index 96480bcb6a5..bdc08988cc0 100644
--- a/doc/user/application_security/dast/browser_based.md
+++ b/doc/user/application_security/dast/browser_based.md
@@ -227,13 +227,13 @@ In such cases, we recommend you disable Anti-CSRF tokens when running a full sca
## Managing scan time
-It is expected that running the browser-based crawler results in better coverage for many web applications, when compared to the normal GitLab DAST solution.
+It is expected that running the browser-based crawler results in better coverage for many web applications, when compared to the standard GitLab DAST solution.
This can come at a cost of increased scan time.
You can manage the trade-off between coverage and scan time with the following measures:
- Limit the number of actions executed by the browser with the [variable](#available-cicd-variables) `DAST_BROWSER_MAX_ACTIONS`. The default is `10,000`.
-- Limit the page depth that the browser-based crawler will check coverage on with the [variable](#available-cicd-variables) `DAST_BROWSER_MAX_DEPTH`. The crawler uses a breadth-first search strategy, so pages with smaller depth are crawled first. The default is `10`.
+- Limit the page depth that the browser-based crawler checks coverage on with the [variable](#available-cicd-variables) `DAST_BROWSER_MAX_DEPTH`. The crawler uses a breadth-first search strategy, so pages with smaller depth are crawled first. The default is `10`.
- Vertically scale the runner and use a higher number of browsers with [variable](#available-cicd-variables) `DAST_BROWSER_NUMBER_OF_BROWSERS`. The default is `3`.
## Timeouts
diff --git a/doc/user/application_security/dast/dast_troubleshooting.md b/doc/user/application_security/dast/dast_troubleshooting.md
index 0dcf203a3a9..da382920604 100644
--- a/doc/user/application_security/dast/dast_troubleshooting.md
+++ b/doc/user/application_security/dast/dast_troubleshooting.md
@@ -77,7 +77,7 @@ For information on this, see the [general Application Security troubleshooting s
To avoid overwriting stages from other CI files, newer versions of the DAST CI template do not
define stages. If you recently started using `DAST.latest.gitlab-ci.yml` or upgraded to a new major
release of GitLab and began receiving this error, you must define a `dast` stage with your other
-stages. Note that you must have a running application for DAST to scan. If your application is set
+stages. You must have a running application for DAST to scan. If your application is set
up in your pipeline, it must be deployed in a stage _before_ the `dast` stage:
```yaml
diff --git a/doc/user/application_security/dast/run_dast_offline.md b/doc/user/application_security/dast/run_dast_offline.md
index 7cb4eff8e68..a75e5832b7c 100644
--- a/doc/user/application_security/dast/run_dast_offline.md
+++ b/doc/user/application_security/dast/run_dast_offline.md
@@ -20,7 +20,7 @@ To use DAST in an offline environment, you need:
[container image](https://gitlab.com/security-products/dast), found in the
[DAST container registry](https://gitlab.com/security-products/dast/container_registry).
-Note that GitLab Runner has a [default `pull policy` of `always`](https://docs.gitlab.com/runner/executors/docker.html#using-the-always-pull-policy),
+GitLab Runner has a [default `pull policy` of `always`](https://docs.gitlab.com/runner/executors/docker.html#using-the-always-pull-policy),
meaning the runner tries to pull Docker images from the GitLab container registry even if a local
copy is available. The GitLab Runner [`pull_policy` can be set to `if-not-present`](https://docs.gitlab.com/runner/executors/docker.html#using-the-if-not-present-pull-policy)
in an offline environment if you prefer using only locally available Docker images. However, we
@@ -34,7 +34,7 @@ For DAST, import the following default DAST analyzer image from `registry.gitlab
- `registry.gitlab.com/security-products/dast:latest`
The process for importing Docker images into a local offline Docker registry depends on
-**your network security policy**. Please consult your IT staff to find an accepted and approved
+**your network security policy**. Consult your IT staff to find an accepted and approved
process by which external resources can be imported or temporarily accessed.
These scanners are [periodically updated](../index.md#vulnerability-scanner-maintenance)
with new definitions, and you may be able to make occasional updates on your own.
diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md
index 4bf5b769c71..0b06da7e4a1 100644
--- a/doc/user/application_security/dependency_scanning/index.md
+++ b/doc/user/application_security/dependency_scanning/index.md
@@ -316,7 +316,7 @@ table.supported-languages ul {
<a id="notes-regarding-supported-languages-and-package-managers-4"></a>
<p>
The presence of a <code>Pipfile.lock</code> file alone will <i>not</i> trigger the analyzer; the presence of a <code>Pipfile</code> is
- still required in order for the analyzer to be executed. However, if a <code>Pipfile.lock</code> file is found, it will be used by
+ still required in order for the analyzer to be executed. However, if a <code>Pipfile.lock</code> file is found, it is used by
<code>Gemnasium</code> to scan the exact package versions listed in this file.
</p>
<p>
@@ -369,7 +369,7 @@ The following package managers use lockfiles that GitLab analyzers are capable o
<li>
<a id="notes-regarding-parsing-lockfiles-1"></a>
<p>
- Dependency Scanning will only parse <code>go.sum</code> if it's unable to generate the build list
+ Dependency Scanning only parses <code>go.sum</code> if it's unable to generate the build list
used by the Go project.
</p>
</li>
@@ -431,7 +431,7 @@ To support the following package managers, the GitLab analyzers proceed in two s
<li>
<a id="exported-dependency-information-notes-3"></a>
<p>
- This test confirms that if a <code>Pipfile.lock</code> file is found, it will be used by <a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium">Gemnasium</a> to scan the exact package versions listed in this file.
+ This test confirms that if a <code>Pipfile.lock</code> file is found, it is used by <a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium">Gemnasium</a> to scan the exact package versions listed in this file.
</p>
</li>
<li>
@@ -659,7 +659,7 @@ The following variables are used for configuring specific analyzers (used for a
#### Other variables
-The previous tables are not an exhaustive list of all variables that can be used. They contain all specific GitLab and analyzer variables we support and test. There are many variables, such as environment variables, that you can pass in and they will work. This is a large list, many of which we may be unaware of, and as such is not documented.
+The previous tables are not an exhaustive list of all variables that can be used. They contain all specific GitLab and analyzer variables we support and test. There are many variables, such as environment variables, that you can pass in and they do work. This is a large list, many of which we may be unaware of, and as such is not documented.
For example, to pass the non-GitLab environment variable `HTTPS_PROXY` to all Dependency Scanning jobs,
set it as a [CI/CD variable in your `.gitlab-ci.yml`](../../../ci/variables/index.md#define-a-cicd-variable-in-the-gitlab-ciyml-file)
@@ -678,7 +678,7 @@ dependency_scanning:
HTTPS_PROXY: $HTTPS_PROXY
```
-As we have not tested all variables you may find some will work and others will not.
+As we have not tested all variables you may find some do work and others do not.
If one does not work and you need it we suggest
[submitting a feature request](https://gitlab.com/gitlab-org/gitlab/-/issues/new?issuable_template=Feature%20proposal%20-%20detailed&issue[title]=Docs%20feedback%20-%20feature%20proposal:%20Write%20your%20title)
or [contributing to the code](../../../development/index.md) to enable it to be used.
@@ -966,7 +966,7 @@ Here are the requirements for using dependency scanning in an offline environmen
This advisory database is constantly being updated, so you must periodically sync your local copy with GitLab.
-Note that GitLab Runner has a [default `pull policy` of `always`](https://docs.gitlab.com/runner/executors/docker.html#using-the-always-pull-policy),
+GitLab Runner has a [default `pull policy` of `always`](https://docs.gitlab.com/runner/executors/docker.html#using-the-always-pull-policy),
meaning the runner tries to pull Docker images from the GitLab container registry even if a local
copy is available. The GitLab Runner [`pull_policy` can be set to `if-not-present`](https://docs.gitlab.com/runner/executors/docker.html#using-the-if-not-present-pull-policy)
in an offline environment if you prefer using only locally available Docker images. However, we
@@ -1262,7 +1262,7 @@ analyzers, edit your `.gitlab-ci.yml` file and either:
[our current Dependency Scanning template](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml#L18).
- If you hardcoded the `DS_ANALYZER_IMAGE` variable directly, change it to match the latest
line as found in our [current Dependency Scanning template](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml).
- The line number will vary depending on which scanning job you edited.
+ The line number varies depending on which scanning job you edited.
For example, currently the `gemnasium-maven-dependency_scanning` job pulls the latest
`gemnasium-maven` Docker image because `DS_ANALYZER_IMAGE` is set to
@@ -1274,7 +1274,7 @@ Support for [2to3](https://docs.python.org/3/library/2to3.html)
was [removed](https://setuptools.pypa.io/en/latest/history.html#v58-0-0)
in `setuptools` version `v58.0.0`. Dependency Scanning (running `python 3.9`) uses `setuptools`
version `58.1.0+`, which doesn't support `2to3`. Therefore, a `setuptools` dependency relying on
-`lib2to3` will fail with this message:
+`lib2to3` fails with this message:
```plaintext
error in <dependency name> setup command: use_2to3 is invalid
@@ -1313,12 +1313,12 @@ To avoid this error, follow [Poetry's configuration advice](https://python-poetr
### Error: Project has `<number>` unresolved dependencies
-The error message `Project has <number> unresolved dependencies` indicates a dependency resolution problem caused by your `gradle.build` or `gradle.build.kts` file. In the current release, `gemnasium-maven` cannot continue processing when an unresolved dependency is encountered. However, There is an [open issue](https://gitlab.com/gitlab-org/gitlab/-/issues/337083) to allow `gemnasium-maven` to recover from unresolved dependency errors and produce a dependency graph. Until this issue has been resolved, you'll need to consult the [Gradle dependency resolution docs](https://docs.gradle.org/current/userguide/dependency_resolution.html) for details on how to fix your `gradle.build` file.
+The error message `Project has <number> unresolved dependencies` indicates a dependency resolution problem caused by your `gradle.build` or `gradle.build.kts` file. In the current release, `gemnasium-maven` cannot continue processing when an unresolved dependency is encountered. However, There is an [open issue](https://gitlab.com/gitlab-org/gitlab/-/issues/337083) to allow `gemnasium-maven` to recover from unresolved dependency errors and produce a dependency graph. Until this issue has been resolved, consult the [Gradle dependency resolution docs](https://docs.gradle.org/current/userguide/dependency_resolution.html) for details on how to fix your `gradle.build` file.
### Setting build constraints when scanning Go projects
Dependency scanning runs within a `linux/amd64` container. As a result, the build list generated
-for a Go project will contain dependencies that are compatible with this environment. If your deployment environment is not
+for a Go project contains dependencies that are compatible with this environment. If your deployment environment is not
`linux/amd64`, the final list of dependencies might contain additional incompatible
modules. The dependency list might also omit modules that are only compatible with your deployment environment. To prevent
this issue, you can configure the build process to target the operating system and architecture of the deployment
diff --git a/doc/user/application_security/policies/scan-execution-policies.md b/doc/user/application_security/policies/scan-execution-policies.md
index f080d5e4661..43b457debb5 100644
--- a/doc/user/application_security/policies/scan-execution-policies.md
+++ b/doc/user/application_security/policies/scan-execution-policies.md
@@ -13,7 +13,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
Group, subgroup, or project owners can use scan execution policies to require that security scans run on a specified
schedule or with the project (or multiple projects if the policy is defined at a group or subgroup level) pipeline. Required scans are injected into the CI pipeline as new jobs
with a long, random job name. In the unlikely event of a job name collision, the security policy job overwrites
-any pre-existing job in the pipeline. If a policy is created at the group-level, it will apply to every child
+any pre-existing job in the pipeline. If a policy is created at the group-level, it applies to every child
project or subgroup. A group-level policy cannot be edited from a child project or subgroup.
This feature has some overlap with [compliance framework pipelines](../../group/compliance_frameworks.md#configure-a-compliance-pipeline),
@@ -88,7 +88,7 @@ This rule enforces the defined actions and schedules a scan on the provided date
| `type` | `string` | `schedule` | The rule's type. |
| `branches` | `array` of `string` | `*` or the branch's name | The branch the given policy applies to (supports wildcard). This field is required if the `agents` field is not set. |
| `cadence` | `string` | CRON expression (for example, `0 0 * * *`) | A whitespace-separated string containing five fields that represents the scheduled time. |
-| `agents` | `object` | | The name of the [GitLab agents](../../clusters/agent/index.md) where [cluster image scanning](../../clusters/agent/vulnerabilities.md) will run. The object key is the name of the Kubernetes agent configured for your project in GitLab. This field is required if the `branches` field is not set. |
+| `agents` | `object` | | The name of the [GitLab agents](../../clusters/agent/index.md) where [cluster image scanning](../../clusters/agent/vulnerabilities.md) runs. The object key is the name of the Kubernetes agent configured for your project in GitLab. This field is required if the `branches` field is not set. |
GitLab supports the following types of CRON syntax for the `cadence` field:
@@ -99,7 +99,7 @@ NOTE:
Other elements of the [CRON syntax](https://docs.oracle.com/cd/E12058_01/doc/doc.1014/e12030/cron_expressions.htm) may work in the cadence field if supported by the [cron](https://github.com/robfig/cron) we are using in our implementation, however, GitLab does not officially test or support them.
NOTE:
-If using the `agents` field, required for `Operational Container Scanning`, the CRON expression is evaluated in [UTC](https://www.timeanddate.com/worldclock/timezone/utc) using the system-time of the Kubernetes-agent pod. If not using the `agents` field, the CRON expression is evaluated in standard [UTC](https://www.timeanddate.com/worldclock/timezone/utc) time from GitLab.com. If you have a self-managed GitLab instance and have [changed the server timezone](../../../administration/timezone.md), the CRON expression is evaluated with the new timezone.
+If using the `agents` field, required for `Operational Container Scanning`, the CRON expression is evaluated in [UTC](https://www.timeanddate.com/worldclock/timezone/utc) using the system-time of the Kubernetes-agent pod. If not using the `agents` field, the CRON expression is evaluated in standard [UTC](https://www.timeanddate.com/worldclock/timezone/utc) time from GitLab.com. If you have a self-managed GitLab instance and have [changed the server time zone](../../../administration/timezone.md), the CRON expression is evaluated with the new time zone.
### `agent` schema
@@ -107,7 +107,7 @@ Use this schema to define `agents` objects in the [`schedule` rule type](#schedu
| Field | Type | Possible values | Description |
|--------------|---------------------|--------------------------|-------------|
-| `namespaces` | `array` of `string` | | The namespace that is scanned. If empty, all namespaces will be scanned. |
+| `namespaces` | `array` of `string` | | The namespace that is scanned. If empty, all namespaces are scanned. |
#### Policy example
@@ -128,9 +128,9 @@ Use this schema to define `agents` objects in the [`schedule` rule type](#schedu
The keys for a schedule rule are:
-- `cadence` (required): a [CRON expression](https://docs.oracle.com/cd/E12058_01/doc/doc.1014/e12030/cron_expressions.htm) for when the scans will be run
+- `cadence` (required): a [CRON expression](https://docs.oracle.com/cd/E12058_01/doc/doc.1014/e12030/cron_expressions.htm) for when the scans are run
- `agents:<agent-name>` (required): The name of the agent to use for scanning
-- `agents:<agent-name>:namespaces` (optional): The Kubernetes namespaces to scan. If omitted, all namespaces will be scanned.
+- `agents:<agent-name>:namespaces` (optional): The Kubernetes namespaces to scan. If omitted, all namespaces are scanned.
## `scan` action type
@@ -143,7 +143,7 @@ rule in the defined policy are met.
| `site_profile` | `string` | Name of the selected [DAST site profile](../dast/proxy-based.md#site-profile). | The DAST site profile to execute the DAST scan. This field should only be set if `scan` type is `dast`. |
| `scanner_profile` | `string` or `null` | Name of the selected [DAST scanner profile](../dast/proxy-based.md#scanner-profile). | The DAST scanner profile to execute the DAST scan. This field should only be set if `scan` type is `dast`.|
| `variables` | `object` | | A set of CI variables, supplied as an array of `key: value` pairs, to apply and enforce for the selected scan. The `key` is the variable name, with its `value` provided as a string. This parameter supports any variable that the GitLab CI job supports for the specified scan. |
-| `tags` | `array` of `string` | | A list of runner tags for the policy. The policy jobs will be run by runner with the specified tags. |
+| `tags` | `array` of `string` | | A list of runner tags for the policy. The policy jobs are run by runner with the specified tags. |
Note the following:
diff --git a/doc/user/clusters/environments.md b/doc/user/clusters/environments.md
index 98292cf9103..f4313656803 100644
--- a/doc/user/clusters/environments.md
+++ b/doc/user/clusters/environments.md
@@ -38,7 +38,7 @@ Access to cluster environments is restricted to
## Usage
-In order to:
+To:
- Track environments for the cluster, you must
[deploy to a Kubernetes cluster](../project/clusters/deploy_to_cluster.md)
diff --git a/spec/frontend/graphql_shared/utils_spec.js b/spec/frontend/graphql_shared/utils_spec.js
index bf899e47d1c..cd334ef0d97 100644
--- a/spec/frontend/graphql_shared/utils_spec.js
+++ b/spec/frontend/graphql_shared/utils_spec.js
@@ -5,6 +5,7 @@ import {
convertToGraphQLIds,
convertFromGraphQLIds,
convertNodeIdsFromGraphQLIds,
+ getNodesOrDefault,
} from '~/graphql_shared/utils';
const mockType = 'Group';
@@ -134,3 +135,28 @@ describe('convertNodeIdsFromGraphQLIds', () => {
);
});
});
+
+describe('getNodesOrDefault', () => {
+ const mockDataWithNodes = {
+ users: {
+ nodes: [
+ { __typename: 'UserCore', id: 'gid://gitlab/User/44' },
+ { __typename: 'UserCore', id: 'gid://gitlab/User/42' },
+ { __typename: 'UserCore', id: 'gid://gitlab/User/41' },
+ ],
+ },
+ };
+
+ it.each`
+ desc | input | expected
+ ${'with nodes child'} | ${[mockDataWithNodes.users]} | ${mockDataWithNodes.users.nodes}
+ ${'with nodes child and "dne" as field'} | ${[mockDataWithNodes.users, 'dne']} | ${[]}
+ ${'with empty data object'} | ${[{ users: {} }]} | ${[]}
+ ${'with empty object'} | ${[{}]} | ${[]}
+ ${'with falsy value'} | ${[undefined]} | ${[]}
+ `('$desc', ({ input, expected }) => {
+ const result = getNodesOrDefault(...input);
+
+ expect(result).toEqual(expected);
+ });
+});
diff --git a/spec/helpers/jira_connect_helper_spec.rb b/spec/helpers/jira_connect_helper_spec.rb
index 97e37023c2d..2685c21a2c0 100644
--- a/spec/helpers/jira_connect_helper_spec.rb
+++ b/spec/helpers/jira_connect_helper_spec.rb
@@ -86,19 +86,6 @@ RSpec.describe JiraConnectHelper do
oauth_token_path: '/oauth/token'
)
end
-
- context 'and jira_connect_oauth_self_managed feature is disabled' do
- before do
- stub_feature_flags(jira_connect_oauth_self_managed: false)
- end
-
- it 'does not point urls to the self-managed instance' do
- expect(parsed_oauth_metadata).not_to include(
- oauth_authorize_url: start_with('https://gitlab.example.com/oauth/authorize?'),
- oauth_token_path: 'https://gitlab.example.com/oauth/token'
- )
- end
- end
end
end
diff --git a/spec/models/concerns/triggerable_hooks_spec.rb b/spec/models/concerns/triggerable_hooks_spec.rb
index 5682a189c41..28cda269458 100644
--- a/spec/models/concerns/triggerable_hooks_spec.rb
+++ b/spec/models/concerns/triggerable_hooks_spec.rb
@@ -9,6 +9,8 @@ RSpec.describe TriggerableHooks do
TestableHook.class_eval do
include TriggerableHooks # rubocop:disable RSpec/DescribedClass
triggerable_hooks [:push_hooks]
+
+ scope :executable, -> { all }
end
end
diff --git a/spec/models/hooks/project_hook_spec.rb b/spec/models/hooks/project_hook_spec.rb
index 3d8c377ab21..9a78d7b1719 100644
--- a/spec/models/hooks/project_hook_spec.rb
+++ b/spec/models/hooks/project_hook_spec.rb
@@ -2,7 +2,19 @@
require 'spec_helper'
-RSpec.describe ProjectHook do
+RSpec.describe ProjectHook, feature_category: :integrations do
+ include_examples 'a hook that gets automatically disabled on failure' do
+ let_it_be(:project) { create(:project) }
+
+ let(:hook) { build(:project_hook, project: project) }
+ let(:hook_factory) { :project_hook }
+ let(:default_factory_arguments) { { project: project } }
+
+ def find_hooks
+ project.hooks
+ end
+ end
+
describe 'associations' do
it { is_expected.to belong_to :project }
end
diff --git a/spec/models/hooks/service_hook_spec.rb b/spec/models/hooks/service_hook_spec.rb
index 2ece04c7158..e52af4a32b0 100644
--- a/spec/models/hooks/service_hook_spec.rb
+++ b/spec/models/hooks/service_hook_spec.rb
@@ -2,7 +2,17 @@
require 'spec_helper'
-RSpec.describe ServiceHook do
+RSpec.describe ServiceHook, feature_category: :integrations do
+ it_behaves_like 'a hook that does not get automatically disabled on failure' do
+ let(:hook) { create(:service_hook) }
+ let(:hook_factory) { :service_hook }
+ let(:default_factory_arguments) { {} }
+
+ def find_hooks
+ described_class.all
+ end
+ end
+
describe 'associations' do
it { is_expected.to belong_to :integration }
end
@@ -11,32 +21,6 @@ RSpec.describe ServiceHook do
it { is_expected.to validate_presence_of(:integration) }
end
- describe 'executable?' do
- let!(:hooks) do
- [
- [0, Time.current],
- [0, 1.minute.from_now],
- [1, 1.minute.from_now],
- [3, 1.minute.from_now],
- [4, nil],
- [4, 1.day.ago],
- [4, 1.minute.from_now],
- [0, nil],
- [0, 1.day.ago],
- [1, nil],
- [1, 1.day.ago],
- [3, nil],
- [3, 1.day.ago]
- ].map do |(recent_failures, disabled_until)|
- create(:service_hook, recent_failures: recent_failures, disabled_until: disabled_until)
- end
- end
-
- it 'is always true' do
- expect(hooks).to all(be_executable)
- end
- end
-
describe 'execute' do
let(:hook) { build(:service_hook) }
let(:data) { { key: 'value' } }
diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb
index ba94730b1dd..edb307148b6 100644
--- a/spec/models/hooks/system_hook_spec.rb
+++ b/spec/models/hooks/system_hook_spec.rb
@@ -2,7 +2,17 @@
require "spec_helper"
-RSpec.describe SystemHook do
+RSpec.describe SystemHook, feature_category: :integrations do
+ it_behaves_like 'a hook that does not get automatically disabled on failure' do
+ let(:hook) { create(:system_hook) }
+ let(:hook_factory) { :system_hook }
+ let(:default_factory_arguments) { {} }
+
+ def find_hooks
+ described_class.all
+ end
+ end
+
context 'default attributes' do
let(:system_hook) { described_class.new }
diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb
index 75ff917c036..600ec5fa936 100644
--- a/spec/models/hooks/web_hook_spec.rb
+++ b/spec/models/hooks/web_hook_spec.rb
@@ -311,88 +311,6 @@ RSpec.describe WebHook, feature_category: :integrations do
end
end
- describe '.executable/.disabled' do
- let!(:not_executable) do
- [
- [0, Time.current],
- [0, 1.minute.from_now],
- [1, 1.minute.from_now],
- [3, 1.minute.from_now],
- [4, nil],
- [4, 1.day.ago],
- [4, 1.minute.from_now]
- ].map do |(recent_failures, disabled_until)|
- create(:project_hook, project: project, recent_failures: recent_failures, disabled_until: disabled_until)
- end
- end
-
- let!(:executables) do
- [
- [0, nil],
- [0, 1.day.ago],
- [1, nil],
- [1, 1.day.ago],
- [3, nil],
- [3, 1.day.ago]
- ].map do |(recent_failures, disabled_until)|
- create(:project_hook, project: project, recent_failures: recent_failures, disabled_until: disabled_until)
- end
- end
-
- it 'finds the correct set of project hooks' do
- expect(described_class.where(project_id: project.id).executable).to match_array executables
- expect(described_class.where(project_id: project.id).disabled).to match_array not_executable
- end
- end
-
- describe '#executable?' do
- let_it_be_with_reload(:web_hook) { create(:project_hook, project: project) }
-
- where(:recent_failures, :not_until, :executable) do
- [
- [0, :not_set, true],
- [0, :past, true],
- [0, :future, true],
- [0, :now, true],
- [1, :not_set, true],
- [1, :past, true],
- [1, :future, true],
- [3, :not_set, true],
- [3, :past, true],
- [3, :future, true],
- [4, :not_set, false],
- [4, :past, true], # expired suspension
- [4, :now, false], # active suspension
- [4, :future, false] # active suspension
- ]
- end
-
- with_them do
- # Phasing means we cannot put these values in the where block,
- # which is not subject to the frozen time context.
- let(:disabled_until) do
- case not_until
- when :not_set
- nil
- when :past
- 1.minute.ago
- when :future
- 1.minute.from_now
- when :now
- Time.current
- end
- end
-
- before do
- web_hook.update!(recent_failures: recent_failures, disabled_until: disabled_until)
- end
-
- it 'has the correct state' do
- expect(web_hook.executable?).to eq(executable)
- end
- end
- end
-
describe '#next_backoff' do
context 'when there was no last backoff' do
before do
@@ -435,50 +353,112 @@ RSpec.describe WebHook, feature_category: :integrations do
end
end
- shared_examples 'is tolerant of invalid records' do
- specify do
- hook.url = nil
+ describe '#rate_limited?' do
+ it 'is false when hook has not been rate limited' do
+ expect_next_instance_of(Gitlab::WebHooks::RateLimiter) do |rate_limiter|
+ expect(rate_limiter).to receive(:rate_limited?).and_return(false)
+ end
+
+ expect(hook).not_to be_rate_limited
+ end
+
+ it 'is true when hook has been rate limited' do
+ expect_next_instance_of(Gitlab::WebHooks::RateLimiter) do |rate_limiter|
+ expect(rate_limiter).to receive(:rate_limited?).and_return(true)
+ end
+
+ expect(hook).to be_rate_limited
+ end
+ end
+
+ describe '#rate_limit' do
+ it 'returns the hook rate limit' do
+ expect_next_instance_of(Gitlab::WebHooks::RateLimiter) do |rate_limiter|
+ expect(rate_limiter).to receive(:limit).and_return(10)
+ end
- expect(hook).to be_invalid
- run_expectation
+ expect(hook.rate_limit).to eq(10)
end
end
- describe '#enable!' do
- it 'makes a hook executable if it was marked as failed' do
- hook.recent_failures = 1000
+ describe '#to_json' do
+ it 'does not error' do
+ expect { hook.to_json }.not_to raise_error
+ end
- expect { hook.enable! }.to change(hook, :executable?).from(false).to(true)
+ it 'does not contain binary attributes' do
+ expect(hook.to_json).not_to include('encrypted_url_variables')
end
+ end
- it 'makes a hook executable if it is currently backed off' do
- hook.recent_failures = 1000
- hook.disabled_until = 1.hour.from_now
+ describe '#interpolated_url' do
+ subject(:hook) { build(:project_hook, project: project) }
- expect { hook.enable! }.to change(hook, :executable?).from(false).to(true)
+ context 'when the hook URL does not contain variables' do
+ before do
+ hook.url = 'http://example.com'
+ end
+
+ it { is_expected.to have_attributes(interpolated_url: hook.url) }
end
- it 'does not update hooks unless necessary' do
- sql_count = ActiveRecord::QueryRecorder.new { hook.enable! }.count
+ it 'is not vulnerable to malicious input' do
+ hook.url = 'something%{%<foo>2147483628G}'
+ hook.url_variables = { 'foo' => '1234567890.12345678' }
- expect(sql_count).to eq(0)
+ expect(hook).to have_attributes(interpolated_url: hook.url)
end
- include_examples 'is tolerant of invalid records' do
- def run_expectation
- hook.recent_failures = 1000
+ context 'when the hook URL contains variables' do
+ before do
+ hook.url = 'http://example.com/{path}/resource?token={token}'
+ hook.url_variables = { 'path' => 'abc', 'token' => 'xyz' }
+ end
+
+ it { is_expected.to have_attributes(interpolated_url: 'http://example.com/abc/resource?token=xyz') }
+
+ context 'when a variable is missing' do
+ before do
+ hook.url_variables = { 'path' => 'present' }
+ end
+
+ it 'raises an error' do
+ # We expect validations to prevent this entirely - this is not user-error
+ expect { hook.interpolated_url }
+ .to raise_error(described_class::InterpolationError, include('Missing key token'))
+ end
+ end
+
+ context 'when the URL appears to include percent formatting' do
+ before do
+ hook.url = 'http://example.com/%{path}/resource?token=%{token}'
+ end
- expect { hook.enable! }.to change(hook, :executable?).from(false).to(true)
+ it 'succeeds, interpolates correctly' do
+ expect(hook.interpolated_url).to eq 'http://example.com/%abc/resource?token=%xyz'
+ end
end
end
end
+ describe '#update_last_failure' do
+ it 'is a method of this class' do
+ expect { described_class.new.update_last_failure }.not_to raise_error
+ end
+ end
+
+ describe '#masked_token' do
+ it { expect(hook.masked_token).to be_nil }
+
+ context 'with a token' do
+ let(:hook) { build(:project_hook, :token, project: project) }
+
+ it { expect(hook.masked_token).to eq described_class::SECRET_MASK }
+ end
+ end
+
describe '#backoff!' do
context 'when we have not backed off before' do
- it 'does not disable the hook' do
- expect { hook.backoff! }.not_to change(hook, :executable?).from(true)
- end
-
it 'increments the recent_failures count' do
expect { hook.backoff! }.to change(hook, :recent_failures).by(1)
end
@@ -517,20 +497,6 @@ RSpec.describe WebHook, feature_category: :integrations do
expect { hook.backoff! }.to change(hook, :backoff_count).by(1)
end
- context 'when the hook is permanently disabled' do
- before do
- allow(hook).to receive(:permanently_disabled?).and_return(true)
- end
-
- it 'does not set disabled_until' do
- expect { hook.backoff! }.not_to change(hook, :disabled_until)
- end
-
- it 'does not increment the backoff count' do
- expect { hook.backoff! }.not_to change(hook, :backoff_count)
- end
- end
-
context 'when we have backed off MAX_FAILURES times' do
before do
stub_const("#{described_class}::MAX_FAILURES", 5)
@@ -554,12 +520,6 @@ RSpec.describe WebHook, feature_category: :integrations do
end
end
end
-
- include_examples 'is tolerant of invalid records' do
- def run_expectation
- expect { hook.backoff! }.to change(hook, :backoff_count).by(1)
- end
- end
end
end
@@ -585,193 +545,5 @@ RSpec.describe WebHook, feature_category: :integrations do
expect(sql_count).to eq(0)
end
-
- include_examples 'is tolerant of invalid records' do
- def run_expectation
- expect { hook.failed! }.to change(hook, :recent_failures).by(1)
- end
- end
- end
-
- describe '#disable!' do
- it 'disables a hook' do
- expect { hook.disable! }.to change(hook, :executable?).from(true).to(false)
- end
-
- include_examples 'is tolerant of invalid records' do
- def run_expectation
- expect { hook.disable! }.to change(hook, :executable?).from(true).to(false)
- end
- end
- end
-
- describe '#temporarily_disabled?' do
- it 'is false when not temporarily disabled' do
- expect(hook).not_to be_temporarily_disabled
- end
-
- it 'allows FAILURE_THRESHOLD initial failures before we back-off' do
- described_class::FAILURE_THRESHOLD.times do
- hook.backoff!
- expect(hook).not_to be_temporarily_disabled
- end
-
- hook.backoff!
- expect(hook).to be_temporarily_disabled
- end
-
- context 'when hook has been told to back off' do
- before do
- hook.update!(recent_failures: described_class::FAILURE_THRESHOLD)
- hook.backoff!
- end
-
- it 'is true' do
- expect(hook).to be_temporarily_disabled
- end
- end
- end
-
- describe '#permanently_disabled?' do
- it 'is false when not disabled' do
- expect(hook).not_to be_permanently_disabled
- end
-
- context 'when hook has been disabled' do
- before do
- hook.disable!
- end
-
- it 'is true' do
- expect(hook).to be_permanently_disabled
- end
- end
- end
-
- describe '#rate_limited?' do
- it 'is false when hook has not been rate limited' do
- expect_next_instance_of(Gitlab::WebHooks::RateLimiter) do |rate_limiter|
- expect(rate_limiter).to receive(:rate_limited?).and_return(false)
- end
-
- expect(hook).not_to be_rate_limited
- end
-
- it 'is true when hook has been rate limited' do
- expect_next_instance_of(Gitlab::WebHooks::RateLimiter) do |rate_limiter|
- expect(rate_limiter).to receive(:rate_limited?).and_return(true)
- end
-
- expect(hook).to be_rate_limited
- end
- end
-
- describe '#rate_limit' do
- it 'returns the hook rate limit' do
- expect_next_instance_of(Gitlab::WebHooks::RateLimiter) do |rate_limiter|
- expect(rate_limiter).to receive(:limit).and_return(10)
- end
-
- expect(hook.rate_limit).to eq(10)
- end
- end
-
- describe '#alert_status' do
- subject(:status) { hook.alert_status }
-
- it { is_expected.to eq :executable }
-
- context 'when hook has been disabled' do
- before do
- hook.disable!
- end
-
- it { is_expected.to eq :disabled }
- end
-
- context 'when hook has been backed off' do
- before do
- hook.update!(recent_failures: described_class::FAILURE_THRESHOLD + 1)
- hook.disabled_until = 1.hour.from_now
- end
-
- it { is_expected.to eq :temporarily_disabled }
- end
- end
-
- describe '#to_json' do
- it 'does not error' do
- expect { hook.to_json }.not_to raise_error
- end
-
- it 'does not contain binary attributes' do
- expect(hook.to_json).not_to include('encrypted_url_variables')
- end
- end
-
- describe '#interpolated_url' do
- subject(:hook) { build(:project_hook, project: project) }
-
- context 'when the hook URL does not contain variables' do
- before do
- hook.url = 'http://example.com'
- end
-
- it { is_expected.to have_attributes(interpolated_url: hook.url) }
- end
-
- it 'is not vulnerable to malicious input' do
- hook.url = 'something%{%<foo>2147483628G}'
- hook.url_variables = { 'foo' => '1234567890.12345678' }
-
- expect(hook).to have_attributes(interpolated_url: hook.url)
- end
-
- context 'when the hook URL contains variables' do
- before do
- hook.url = 'http://example.com/{path}/resource?token={token}'
- hook.url_variables = { 'path' => 'abc', 'token' => 'xyz' }
- end
-
- it { is_expected.to have_attributes(interpolated_url: 'http://example.com/abc/resource?token=xyz') }
-
- context 'when a variable is missing' do
- before do
- hook.url_variables = { 'path' => 'present' }
- end
-
- it 'raises an error' do
- # We expect validations to prevent this entirely - this is not user-error
- expect { hook.interpolated_url }
- .to raise_error(described_class::InterpolationError, include('Missing key token'))
- end
- end
-
- context 'when the URL appears to include percent formatting' do
- before do
- hook.url = 'http://example.com/%{path}/resource?token=%{token}'
- end
-
- it 'succeeds, interpolates correctly' do
- expect(hook.interpolated_url).to eq 'http://example.com/%abc/resource?token=%xyz'
- end
- end
- end
- end
-
- describe '#update_last_failure' do
- it 'is a method of this class' do
- expect { described_class.new.update_last_failure }.not_to raise_error
- end
- end
-
- describe '#masked_token' do
- it { expect(hook.masked_token).to be_nil }
-
- context 'with a token' do
- let(:hook) { build(:project_hook, :token, project: project) }
-
- it { expect(hook.masked_token).to eq described_class::SECRET_MASK }
- end
end
end
diff --git a/spec/models/jira_connect_installation_spec.rb b/spec/models/jira_connect_installation_spec.rb
index 525690fa6b7..6cd1534c0c8 100644
--- a/spec/models/jira_connect_installation_spec.rb
+++ b/spec/models/jira_connect_installation_spec.rb
@@ -85,14 +85,6 @@ RSpec.describe JiraConnectInstallation, feature_category: :integrations do
let(:installation) { build(:jira_connect_installation, instance_url: 'https://gitlab.example.com') }
it { is_expected.to eq('https://gitlab.example.com') }
-
- context 'and jira_connect_oauth_self_managed feature is disabled' do
- before do
- stub_feature_flags(jira_connect_oauth_self_managed: false)
- end
-
- it { is_expected.to eq('http://test.host') }
- end
end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 340b859ee5c..63931f136f8 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -5933,6 +5933,18 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
project.execute_hooks(data, :push_hooks)
end
+ it 'executes hooks which were backed off and are no longer backed off' do
+ project = create(:project)
+ hook = create(:project_hook, project: project, push_events: true)
+ WebHook::FAILURE_THRESHOLD.succ.times { hook.backoff! }
+
+ expect_any_instance_of(ProjectHook).to receive(:async_execute).once
+
+ travel_to(hook.disabled_until + 1.second) do
+ project.execute_hooks(data, :push_hooks)
+ end
+ end
+
it 'executes the system hooks with the specified scope' do
expect_any_instance_of(SystemHooksService).to receive(:execute_hooks).with(data, :merge_request_hooks)
diff --git a/spec/requests/jira_connect/public_keys_controller_spec.rb b/spec/requests/jira_connect/public_keys_controller_spec.rb
index bf472469d85..31977f34d0f 100644
--- a/spec/requests/jira_connect/public_keys_controller_spec.rb
+++ b/spec/requests/jira_connect/public_keys_controller_spec.rb
@@ -38,18 +38,6 @@ RSpec.describe JiraConnect::PublicKeysController, feature_category: :integration
expect(response).to have_gitlab_http_status(:not_found)
end
end
-
- context 'when jira_connect_oauth_self_managed disabled' do
- before do
- stub_feature_flags(jira_connect_oauth_self_managed: false)
- end
-
- it 'renders 404' do
- get jira_connect_public_key_path(id: uuid)
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
end
end
end
diff --git a/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb b/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb
new file mode 100644
index 00000000000..122774a9028
--- /dev/null
+++ b/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb
@@ -0,0 +1,272 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'a hook that gets automatically disabled on failure' do
+ shared_examples 'is tolerant of invalid records' do
+ specify do
+ hook.url = nil
+
+ expect(hook).to be_invalid
+ run_expectation
+ end
+ end
+
+ describe '.executable/.disabled', :freeze_time do
+ let!(:not_executable) do
+ [
+ [4, nil], # Exceeded the grace period, set by #fail!
+ [4, 1.second.from_now], # Exceeded the grace period, set by #backoff!
+ [4, Time.current] # Exceeded the grace period, set by #backoff!, edge-case
+ ].map do |(recent_failures, disabled_until)|
+ create(hook_factory, **default_factory_arguments, recent_failures: recent_failures,
+disabled_until: disabled_until)
+ end
+ end
+
+ let!(:executables) do
+ expired = 1.second.ago
+ borderline = Time.current
+ suspended = 1.second.from_now
+
+ [
+ # Most of these are impossible states, but are included for completeness
+ [0, nil],
+ [1, nil],
+ [3, nil],
+ [4, expired],
+
+ # Impossible cases:
+ [3, suspended],
+ [3, expired],
+ [3, borderline],
+ [1, suspended],
+ [1, expired],
+ [1, borderline],
+ [0, borderline],
+ [0, suspended],
+ [0, expired]
+ ].map do |(recent_failures, disabled_until)|
+ create(hook_factory, **default_factory_arguments, recent_failures: recent_failures,
+disabled_until: disabled_until)
+ end
+ end
+
+ it 'finds the correct set of project hooks' do
+ expect(find_hooks.executable).to match_array executables
+ expect(find_hooks.executable).to all(be_executable)
+
+ # As expected, and consistent
+ expect(find_hooks.disabled).to match_array not_executable
+ expect(find_hooks.disabled.map(&:executable?)).not_to include(true)
+
+ # Nothing is missing
+ expect(find_hooks.executable.to_a + find_hooks.disabled.to_a).to match_array(find_hooks.to_a)
+ end
+ end
+
+ describe '#executable?', :freeze_time do
+ let(:web_hook) { create(hook_factory, **default_factory_arguments) }
+
+ where(:recent_failures, :not_until, :executable) do
+ [
+ [0, :not_set, true],
+ [0, :past, true],
+ [0, :future, true],
+ [0, :now, true],
+ [1, :not_set, true],
+ [1, :past, true],
+ [1, :future, true],
+ [3, :not_set, true],
+ [3, :past, true],
+ [3, :future, true],
+ [4, :not_set, false],
+ [4, :past, true], # expired suspension
+ [4, :now, false], # active suspension
+ [4, :future, false] # active suspension
+ ]
+ end
+
+ with_them do
+ # Phasing means we cannot put these values in the where block,
+ # which is not subject to the frozen time context.
+ let(:disabled_until) do
+ case not_until
+ when :not_set
+ nil
+ when :past
+ 1.minute.ago
+ when :future
+ 1.minute.from_now
+ when :now
+ Time.current
+ end
+ end
+
+ before do
+ web_hook.update!(recent_failures: recent_failures, disabled_until: disabled_until)
+ end
+
+ it 'has the correct state' do
+ expect(web_hook.executable?).to eq(executable)
+ end
+ end
+ end
+
+ describe '#enable!' do
+ it 'makes a hook executable if it was marked as failed' do
+ hook.recent_failures = 1000
+
+ expect { hook.enable! }.to change { hook.executable? }.from(false).to(true)
+ end
+
+ it 'makes a hook executable if it is currently backed off' do
+ hook.recent_failures = 1000
+ hook.disabled_until = 1.hour.from_now
+
+ expect { hook.enable! }.to change { hook.executable? }.from(false).to(true)
+ end
+
+ it 'does not update hooks unless necessary' do
+ hook
+
+ sql_count = ActiveRecord::QueryRecorder.new { hook.enable! }.count
+
+ expect(sql_count).to eq(0)
+ end
+
+ include_examples 'is tolerant of invalid records' do
+ def run_expectation
+ hook.recent_failures = 1000
+
+ expect { hook.enable! }.to change { hook.executable? }.from(false).to(true)
+ end
+ end
+ end
+
+ describe '#backoff!' do
+ context 'when we have not backed off before' do
+ it 'does not disable the hook' do
+ expect { hook.backoff! }.not_to change { hook.executable? }.from(true)
+ end
+ end
+
+ context 'when we have exhausted the grace period' do
+ before do
+ hook.update!(recent_failures: WebHook::FAILURE_THRESHOLD)
+ end
+
+ context 'when the hook is permanently disabled' do
+ before do
+ allow(hook).to receive(:permanently_disabled?).and_return(true)
+ end
+
+ it 'does not set disabled_until' do
+ expect { hook.backoff! }.not_to change { hook.disabled_until }
+ end
+
+ it 'does not increment the backoff count' do
+ expect { hook.backoff! }.not_to change { hook.backoff_count }
+ end
+ end
+
+ include_examples 'is tolerant of invalid records' do
+ def run_expectation
+ expect { hook.backoff! }.to change { hook.backoff_count }.by(1)
+ end
+ end
+ end
+ end
+
+ describe '#failed!' do
+ include_examples 'is tolerant of invalid records' do
+ def run_expectation
+ expect { hook.failed! }.to change { hook.recent_failures }.by(1)
+ end
+ end
+ end
+
+ describe '#disable!' do
+ it 'disables a hook' do
+ expect { hook.disable! }.to change { hook.executable? }.from(true).to(false)
+ end
+
+ it 'does nothing if the hook is already disabled' do
+ allow(hook).to receive(:permanently_disabled?).and_return(true)
+
+ sql_count = ActiveRecord::QueryRecorder.new { hook.disable! }.count
+
+ expect(sql_count).to eq(0)
+ end
+
+ include_examples 'is tolerant of invalid records' do
+ def run_expectation
+ expect { hook.disable! }.to change { hook.executable? }.from(true).to(false)
+ end
+ end
+ end
+
+ describe '#temporarily_disabled?' do
+ it 'is false when not temporarily disabled' do
+ expect(hook).not_to be_temporarily_disabled
+ end
+
+ it 'allows FAILURE_THRESHOLD initial failures before we back-off' do
+ WebHook::FAILURE_THRESHOLD.times do
+ hook.backoff!
+ expect(hook).not_to be_temporarily_disabled
+ end
+
+ hook.backoff!
+ expect(hook).to be_temporarily_disabled
+ end
+
+ context 'when hook has been told to back off' do
+ before do
+ hook.update!(recent_failures: WebHook::FAILURE_THRESHOLD)
+ hook.backoff!
+ end
+
+ it 'is true' do
+ expect(hook).to be_temporarily_disabled
+ end
+ end
+ end
+
+ describe '#permanently_disabled?' do
+ it 'is false when not disabled' do
+ expect(hook).not_to be_permanently_disabled
+ end
+
+ context 'when hook has been disabled' do
+ before do
+ hook.disable!
+ end
+
+ it 'is true' do
+ expect(hook).to be_permanently_disabled
+ end
+ end
+ end
+
+ describe '#alert_status' do
+ subject(:status) { hook.alert_status }
+
+ it { is_expected.to eq :executable }
+
+ context 'when hook has been disabled' do
+ before do
+ hook.disable!
+ end
+
+ it { is_expected.to eq :disabled }
+ end
+
+ context 'when hook has been backed off' do
+ before do
+ hook.update!(recent_failures: WebHook::FAILURE_THRESHOLD + 1)
+ hook.disabled_until = 1.hour.from_now
+ end
+
+ it { is_expected.to eq :temporarily_disabled }
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/concerns/unstoppable_hooks_shared_examples.rb b/spec/support/shared_examples/models/concerns/unstoppable_hooks_shared_examples.rb
new file mode 100644
index 00000000000..848840ee297
--- /dev/null
+++ b/spec/support/shared_examples/models/concerns/unstoppable_hooks_shared_examples.rb
@@ -0,0 +1,177 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'a hook that does not get automatically disabled on failure' do
+ describe '.executable/.disabled', :freeze_time do
+ let!(:executables) do
+ [
+ [0, Time.current],
+ [0, 1.minute.from_now],
+ [1, 1.minute.from_now],
+ [3, 1.minute.from_now],
+ [4, nil],
+ [4, 1.day.ago],
+ [4, 1.minute.from_now],
+ [0, nil],
+ [0, 1.day.ago],
+ [1, nil],
+ [1, 1.day.ago],
+ [3, nil],
+ [3, 1.day.ago]
+ ].map do |(recent_failures, disabled_until)|
+ create(hook_factory, **default_factory_arguments, recent_failures: recent_failures,
+disabled_until: disabled_until)
+ end
+ end
+
+ it 'finds the correct set of project hooks' do
+ expect(find_hooks).to all(be_executable)
+ expect(find_hooks.executable).to match_array executables
+ expect(find_hooks.disabled).to be_empty
+ end
+ end
+
+ describe '#executable?', :freeze_time do
+ let(:web_hook) { create(hook_factory, **default_factory_arguments) }
+
+ where(:recent_failures, :not_until) do
+ [
+ [0, :not_set],
+ [0, :past],
+ [0, :future],
+ [0, :now],
+ [1, :not_set],
+ [1, :past],
+ [1, :future],
+ [3, :not_set],
+ [3, :past],
+ [3, :future],
+ [4, :not_set],
+ [4, :past], # expired suspension
+ [4, :now], # active suspension
+ [4, :future] # active suspension
+ ]
+ end
+
+ with_them do
+ # Phasing means we cannot put these values in the where block,
+ # which is not subject to the frozen time context.
+ let(:disabled_until) do
+ case not_until
+ when :not_set
+ nil
+ when :past
+ 1.minute.ago
+ when :future
+ 1.minute.from_now
+ when :now
+ Time.current
+ end
+ end
+
+ before do
+ web_hook.update!(recent_failures: recent_failures, disabled_until: disabled_until)
+ end
+
+ it 'has the correct state' do
+ expect(web_hook).to be_executable
+ end
+ end
+ end
+
+ describe '#enable!' do
+ it 'makes a hook executable if it was marked as failed' do
+ hook.recent_failures = 1000
+
+ expect { hook.enable! }.not_to change { hook.executable? }.from(true)
+ end
+
+ it 'makes a hook executable if it is currently backed off' do
+ hook.recent_failures = 1000
+ hook.disabled_until = 1.hour.from_now
+
+ expect { hook.enable! }.not_to change { hook.executable? }.from(true)
+ end
+
+ it 'does not update hooks unless necessary' do
+ hook
+
+ sql_count = ActiveRecord::QueryRecorder.new { hook.enable! }.count
+
+ expect(sql_count).to eq(0)
+ end
+ end
+
+ describe '#backoff!' do
+ context 'when we have not backed off before' do
+ it 'does not disable the hook' do
+ expect { hook.backoff! }.not_to change { hook.executable? }.from(true)
+ end
+ end
+
+ context 'when we have exhausted the grace period' do
+ before do
+ hook.update!(recent_failures: WebHook::FAILURE_THRESHOLD)
+ end
+
+ it 'does not disable the hook' do
+ expect { hook.backoff! }.not_to change { hook.executable? }.from(true)
+ end
+ end
+ end
+
+ describe '#disable!' do
+ it 'does not disable a group hook' do
+ expect { hook.disable! }.not_to change { hook.executable? }.from(true)
+ end
+ end
+
+ describe '#temporarily_disabled?' do
+ it 'is false' do
+ # Initially
+ expect(hook).not_to be_temporarily_disabled
+
+ # Backing off
+ WebHook::FAILURE_THRESHOLD.times do
+ hook.backoff!
+ expect(hook).not_to be_temporarily_disabled
+ end
+
+ hook.backoff!
+ expect(hook).not_to be_temporarily_disabled
+ end
+ end
+
+ describe '#permanently_disabled?' do
+ it 'is false' do
+ # Initially
+ expect(hook).not_to be_permanently_disabled
+
+ hook.disable!
+
+ expect(hook).not_to be_permanently_disabled
+ end
+ end
+
+ describe '#alert_status' do
+ subject(:status) { hook.alert_status }
+
+ it { is_expected.to eq :executable }
+
+ context 'when hook has been disabled' do
+ before do
+ hook.disable!
+ end
+
+ it { is_expected.to eq :executable }
+ end
+
+ context 'when hook has been backed off' do
+ before do
+ hook.update!(recent_failures: WebHook::FAILURE_THRESHOLD + 1)
+ hook.disabled_until = 1.hour.from_now
+ end
+
+ it { is_expected.to eq :executable }
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/hooks_shared_examples.rb b/spec/support/shared_examples/requests/api/hooks_shared_examples.rb
index d666a754d9f..f2002de4b55 100644
--- a/spec/support/shared_examples/requests/api/hooks_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/hooks_shared_examples.rb
@@ -128,7 +128,8 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix|
get api(hook_uri, user)
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to include('alert_status' => 'disabled')
+
+ expect(json_response).to include('alert_status' => 'disabled') unless hook.executable?
end
end
@@ -142,10 +143,13 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix|
get api(hook_uri, user)
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to include(
- 'alert_status' => 'temporarily_disabled',
- 'disabled_until' => hook.disabled_until.iso8601(3)
- )
+
+ unless hook.executable?
+ expect(json_response).to include(
+ 'alert_status' => 'temporarily_disabled',
+ 'disabled_until' => hook.disabled_until.iso8601(3)
+ )
+ end
end
end
end