Commit 77019efa authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents c9aa0b7b b4c3ebcd
...@@ -48,11 +48,12 @@ export default class AccessDropdown { ...@@ -48,11 +48,12 @@ export default class AccessDropdown {
clicked: options => { clicked: options => {
const { $el, e } = options; const { $el, e } = options;
const item = options.selectedObj; const item = options.selectedObj;
const fossWithMergeAccess = !this.hasLicense && this.accessLevel === ACCESS_LEVELS.MERGE;
e.preventDefault(); e.preventDefault();
if (!this.hasLicense) { if (fossWithMergeAccess) {
// We're not multiselecting quite yet with FOSS: // We're not multiselecting quite yet in "Merge" access dropdown, on FOSS:
// remove all preselected items before selecting this item // remove all preselected items before selecting this item
// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37499 // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37499
this.accessLevelsData.forEach(level => { this.accessLevelsData.forEach(level => {
...@@ -62,7 +63,7 @@ export default class AccessDropdown { ...@@ -62,7 +63,7 @@ export default class AccessDropdown {
if ($el.is('.is-active')) { if ($el.is('.is-active')) {
if (this.noOneObj) { if (this.noOneObj) {
if (item.id === this.noOneObj.id && this.hasLicense) { if (item.id === this.noOneObj.id && !fossWithMergeAccess) {
// remove all others selected items // remove all others selected items
this.accessLevelsData.forEach(level => { this.accessLevelsData.forEach(level => {
if (level.id !== item.id) { if (level.id !== item.id) {
......
...@@ -149,8 +149,8 @@ module Clusters ...@@ -149,8 +149,8 @@ module Clusters
scope :for_project_namespace, -> (namespace_id) { joins(:projects).where(projects: { namespace_id: namespace_id }) } scope :for_project_namespace, -> (namespace_id) { joins(:projects).where(projects: { namespace_id: namespace_id }) }
scope :with_application_prometheus, -> { includes(:application_prometheus).joins(:application_prometheus) } scope :with_application_prometheus, -> { includes(:application_prometheus).joins(:application_prometheus) }
scope :with_project_alert_service_data, -> (project_ids) do scope :with_project_http_integrations, -> (project_ids) do
conditions = { projects: { alerts_service: [:data] } } conditions = { projects: :alert_management_http_integrations }
includes(conditions).joins(conditions).where(projects: { id: project_ids }) includes(conditions).joins(conditions).where(projects: { id: project_ids })
end end
......
...@@ -12,6 +12,10 @@ module ProtectedRef ...@@ -12,6 +12,10 @@ module ProtectedRef
delegate :matching, :matches?, :wildcard?, to: :ref_matcher delegate :matching, :matches?, :wildcard?, to: :ref_matcher
scope :for_project, ->(project) { where(project: project) } scope :for_project, ->(project) { where(project: project) }
def allow_multiple?(type)
false
end
end end
def commit def commit
...@@ -29,7 +33,7 @@ module ProtectedRef ...@@ -29,7 +33,7 @@ module ProtectedRef
# to fail. # to fail.
has_many :"#{type}_access_levels", inverse_of: self.model_name.singular has_many :"#{type}_access_levels", inverse_of: self.model_name.singular
validates :"#{type}_access_levels", length: { is: 1, message: "are restricted to a single instance per #{self.model_name.human}." } validates :"#{type}_access_levels", length: { is: 1, message: "are restricted to a single instance per #{self.model_name.human}." }, unless: -> { allow_multiple?(type) }
accepts_nested_attributes_for :"#{type}_access_levels", allow_destroy: true accepts_nested_attributes_for :"#{type}_access_levels", allow_destroy: true
end end
......
...@@ -45,6 +45,7 @@ module ProtectedRefAccess ...@@ -45,6 +45,7 @@ module ProtectedRefAccess
end end
def check_access(user) def check_access(user)
return false unless user
return true if user.admin? return true if user.admin?
user.can?(:push_code, project) && user.can?(:push_code, project) &&
......
...@@ -48,6 +48,10 @@ class ProtectedBranch < ApplicationRecord ...@@ -48,6 +48,10 @@ class ProtectedBranch < ApplicationRecord
where(fuzzy_arel_match(:name, query.downcase)) where(fuzzy_arel_match(:name, query.downcase))
end end
def allow_multiple?(type)
type == :push
end
end end
ProtectedBranch.prepend_if_ee('EE::ProtectedBranch') ProtectedBranch.prepend_if_ee('EE::ProtectedBranch')
...@@ -37,8 +37,6 @@ class ProtectedBranch::PushAccessLevel < ApplicationRecord ...@@ -37,8 +37,6 @@ class ProtectedBranch::PushAccessLevel < ApplicationRecord
end end
def enabled_deploy_key_for_user?(deploy_key, user) def enabled_deploy_key_for_user?(deploy_key, user)
return false unless deploy_key.user_id == user.id deploy_key.user_id == user.id && DeployKey.with_write_access_for_project(protected_branch.project, deploy_key: deploy_key).any?
DeployKey.with_write_access_for_project(protected_branch.project, deploy_key: deploy_key).any?
end end
end end
...@@ -63,8 +63,10 @@ module Clusters ...@@ -63,8 +63,10 @@ module Clusters
def send_notification(project) def send_notification(project)
notification_payload = build_notification_payload(project) notification_payload = build_notification_payload(project)
token = project.alerts_service.data.token integration = project.alert_management_http_integrations.active.first
Projects::Alerting::NotifyService.new(project, nil, notification_payload).execute(token)
Projects::Alerting::NotifyService.new(project, nil, notification_payload).execute(integration&.token, integration)
@logger.info(message: 'Successfully notified of Prometheus newly unhealthy', cluster_id: @cluster.id, project_id: project.id) @logger.info(message: 'Successfully notified of Prometheus newly unhealthy', cluster_id: @cluster.id, project_id: project.id)
end end
......
- select_mode_for_dropdown = Feature.enabled?(:deploy_keys_on_protected_branches, protected_branch.project) ? 'js-multiselect' : ''
- merge_access_levels = protected_branch.merge_access_levels.for_role - merge_access_levels = protected_branch.merge_access_levels.for_role
- push_access_levels = protected_branch.push_access_levels.for_role - push_access_levels = protected_branch.push_access_levels.for_role
...@@ -23,7 +25,7 @@ ...@@ -23,7 +25,7 @@
%td.push_access_levels-container %td.push_access_levels-container
= hidden_field_tag "allowed_to_push_#{protected_branch.id}", push_access_levels.first&.access_level = hidden_field_tag "allowed_to_push_#{protected_branch.id}", push_access_levels.first&.access_level
= dropdown_tag( (push_access_levels.first&.humanize || 'Select') , = dropdown_tag( (push_access_levels.first&.humanize || 'Select') ,
options: { toggle_class: 'js-allowed-to-push', dropdown_class: 'dropdown-menu-selectable js-allowed-to-push-container capitalize-header', options: { toggle_class: "js-allowed-to-push #{select_mode_for_dropdown}", dropdown_class: 'dropdown-menu-selectable js-allowed-to-push-container capitalize-header',
data: { field_name: "allowed_to_push_#{protected_branch.id}", preselected_items: access_levels_data(push_access_levels) }}) data: { field_name: "allowed_to_push_#{protected_branch.id}", preselected_items: access_levels_data(push_access_levels) }})
- if user_push_access_levels.any? - if user_push_access_levels.any?
%p.small %p.small
......
...@@ -20,7 +20,7 @@ module Clusters ...@@ -20,7 +20,7 @@ module Clusters
demo_project_ids = Gitlab::Monitor::DemoProjects.primary_keys demo_project_ids = Gitlab::Monitor::DemoProjects.primary_keys
clusters = Clusters::Cluster.with_application_prometheus clusters = Clusters::Cluster.with_application_prometheus
.with_project_alert_service_data(demo_project_ids) .with_project_http_integrations(demo_project_ids)
# Move to a seperate worker with scoped context if expanded to do work on customer projects # Move to a seperate worker with scoped context if expanded to do work on customer projects
clusters.each { |cluster| Clusters::Applications::PrometheusHealthCheckService.new(cluster).execute } clusters.each { |cluster| Clusters::Applications::PrometheusHealthCheckService.new(cluster).execute }
......
...@@ -12,7 +12,7 @@ recommended for ease of future upgrades or keeping the data you create. ...@@ -12,7 +12,7 @@ recommended for ease of future upgrades or keeping the data you create.
## Initial setup ## Initial setup
In this guide you'll configure a Digital Ocean droplet and set up Docker This guide configures a Digital Ocean droplet and sets up Docker
locally on either macOS or Linux. locally on either macOS or Linux.
### On macOS ### On macOS
...@@ -39,10 +39,10 @@ The rest of the steps are identical for macOS and Linux. ...@@ -39,10 +39,10 @@ The rest of the steps are identical for macOS and Linux.
1. Login to Digital Ocean. 1. Login to Digital Ocean.
1. Generate a new API token at <https://cloud.digitalocean.com/settings/api/tokens>. 1. Generate a new API token at <https://cloud.digitalocean.com/settings/api/tokens>.
This command will create a new DO droplet called `gitlab-test-env-do` that will act as a Docker host. This command creates a new Digital Ocean droplet called `gitlab-test-env-do` that acts as a Docker host.
NOTE: NOTE:
4GB is the minimum requirement for a Docker host that will run more than one GitLab instance. 4GB is the minimum requirement for a Docker host that runs more than one GitLab instance.
- RAM: 4GB - RAM: 4GB
- Name: `gitlab-test-env-do` - Name: `gitlab-test-env-do`
...@@ -70,7 +70,7 @@ Resource: <https://docs.docker.com/machine/drivers/digital-ocean/>. ...@@ -70,7 +70,7 @@ Resource: <https://docs.docker.com/machine/drivers/digital-ocean/>.
### Connect your shell to the new machine ### Connect your shell to the new machine
In this example we'll create a GitLab EE 8.10.8 instance. This example creates a GitLab EE 8.10.8 instance.
First connect the Docker client to the Docker host you created previously. First connect the Docker client to the Docker host you created previously.
......
...@@ -10,7 +10,7 @@ WARNING: ...@@ -10,7 +10,7 @@ WARNING:
As of September 13, 2017, the GitLab Enterprise Plus for Pivotal Cloud Foundry As of September 13, 2017, the GitLab Enterprise Plus for Pivotal Cloud Foundry
tile on Pivotal Network has reached its End of Availability (“EoA”) and is no tile on Pivotal Network has reached its End of Availability (“EoA”) and is no
longer available for download or sale through Pivotal. Current customers with longer available for download or sale through Pivotal. Current customers with
active subscriptions will continue to receive support from GitLab through their active subscriptions continue to receive support from GitLab through their
subscription term. Pivotal and GitLab are collaborating on creating a new subscription term. Pivotal and GitLab are collaborating on creating a new
Kubernetes-based tile for the Pivotal Container Service. Please contact GitLab Kubernetes-based tile for the Pivotal Container Service. Please contact GitLab
support with any questions regarding GitLab Enterprise Plus for Pivotal Cloud Foundry. support with any questions regarding GitLab Enterprise Plus for Pivotal Cloud Foundry.
......
...@@ -40,9 +40,9 @@ to endpoints like `http://localhost:123/some-resource/delete`. ...@@ -40,9 +40,9 @@ to endpoints like `http://localhost:123/some-resource/delete`.
To prevent this type of exploitation from happening, starting with GitLab 10.6, To prevent this type of exploitation from happening, starting with GitLab 10.6,
all Webhook requests to the current GitLab instance server address and/or in a all Webhook requests to the current GitLab instance server address and/or in a
private network will be forbidden by default. That means that all requests made private network are forbidden by default. That means that all requests made
to `127.0.0.1`, `::1` and `0.0.0.0`, as well as IPv4 `10.0.0.0/8`, `172.16.0.0/12`, to `127.0.0.1`, `::1` and `0.0.0.0`, as well as IPv4 `10.0.0.0/8`, `172.16.0.0/12`,
`192.168.0.0/16` and IPv6 site-local (`ffc0::/10`) addresses won't be allowed. `192.168.0.0/16` and IPv6 site-local (`ffc0::/10`) addresses aren't allowed.
This behavior can be overridden by enabling the option *"Allow requests to the This behavior can be overridden by enabling the option *"Allow requests to the
local network from web hooks and services"* in the *"Outbound requests"* section local network from web hooks and services"* in the *"Outbound requests"* section
...@@ -75,9 +75,9 @@ The allowlist can hold a maximum of 1000 entries. Each entry can be a maximum of ...@@ -75,9 +75,9 @@ The allowlist can hold a maximum of 1000 entries. Each entry can be a maximum of
255 characters. 255 characters.
You can allow a particular port by specifying it in the allowlist entry. You can allow a particular port by specifying it in the allowlist entry.
For example `127.0.0.1:8080` will only allow connections to port 8080 on `127.0.0.1`. For example `127.0.0.1:8080` only allows connections to port 8080 on `127.0.0.1`.
If no port is mentioned, all ports on that IP/domain are allowed. An IP range If no port is mentioned, all ports on that IP/domain are allowed. An IP range
will allow all ports on all IPs in that range. allows all ports on all IPs in that range.
Example: Example:
......
...@@ -249,7 +249,7 @@ Please refer to `group_rename` and `user_rename` for that case. ...@@ -249,7 +249,7 @@ Please refer to `group_rename` and `user_rename` for that case.
} }
``` ```
If the user is blocked via LDAP, `state` will be `ldap_blocked`. If the user is blocked via LDAP, `state` is `ldap_blocked`.
**User renamed:** **User renamed:**
......
...@@ -8,13 +8,13 @@ type: howto, reference ...@@ -8,13 +8,13 @@ type: howto, reference
# Email from GitLab **(STARTER ONLY)** # Email from GitLab **(STARTER ONLY)**
GitLab provides a simple tool to administrators for emailing all users, or users of GitLab provides a simple tool to administrators for emailing all users, or users of
a chosen group or project, right from the Admin Area. Users will receive the email a chosen group or project, right from the Admin Area. Users receive the email
at their primary email address. at their primary email address.
## Use-cases ## Use-cases
- Notify your users about a new project, a new feature, or a new product launch. - Notify your users about a new project, a new feature, or a new product launch.
- Notify your users about a new deployment, or that will be downtime expected - Notify your users about a new deployment, or that downtime is expected
for a particular reason. for a particular reason.
## Sending emails to users from within GitLab ## Sending emails to users from within GitLab
...@@ -24,9 +24,9 @@ at their primary email address. ...@@ -24,9 +24,9 @@ at their primary email address.
![admin users](email1.png) ![admin users](email1.png)
1. Compose an email and choose where it will be sent (all users or users of a 1. Compose an email and choose where to send it (all users or users of a
chosen group or project). The email body only supports plain text messages. chosen group or project). The email body only supports plain text messages.
HTML, Markdown, and other rich text formats are not supported, and will be HTML, Markdown, and other rich text formats are not supported, and is
sent as plain text to users. sent as plain text to users.
![compose an email](email2.png) ![compose an email](email2.png)
...@@ -40,7 +40,7 @@ Users can choose to unsubscribe from receiving emails from GitLab by following ...@@ -40,7 +40,7 @@ Users can choose to unsubscribe from receiving emails from GitLab by following
the unsubscribe link in the email. Unsubscribing is unauthenticated in order the unsubscribe link in the email. Unsubscribing is unauthenticated in order
to keep this feature simple. to keep this feature simple.
On unsubscribe, users will receive an email notification that unsubscribe happened. On unsubscribe, users receive an email notification that unsubscribe happened.
The endpoint that provides the unsubscribe option is rate-limited. The endpoint that provides the unsubscribe option is rate-limited.
<!-- ## Troubleshooting <!-- ## Troubleshooting
......
...@@ -51,7 +51,7 @@ For other distributions, follow the instructions in PostgreSQL's ...@@ -51,7 +51,7 @@ For other distributions, follow the instructions in PostgreSQL's
[download page](https://www.postgresql.org/download/) to add their repository [download page](https://www.postgresql.org/download/) to add their repository
and then install `pgloader`. and then install `pgloader`.
If you are migrating to a Docker based installation, you will need to install If you are migrating to a Docker based installation, you must install
pgloader within the container as it is not included in the container image. pgloader within the container as it is not included in the container image.
1. Start a shell session in the context of the running container: 1. Start a shell session in the context of the running container:
...@@ -69,7 +69,7 @@ pgloader within the container as it is not included in the container image. ...@@ -69,7 +69,7 @@ pgloader within the container as it is not included in the container image.
## Omnibus GitLab installations ## Omnibus GitLab installations
For [Omnibus GitLab packages](https://about.gitlab.com/install/), you'll first For [Omnibus GitLab packages](https://about.gitlab.com/install/), you first
need to enable the bundled PostgreSQL: need to enable the bundled PostgreSQL:
1. Stop GitLab: 1. Stop GitLab:
...@@ -84,13 +84,13 @@ need to enable the bundled PostgreSQL: ...@@ -84,13 +84,13 @@ need to enable the bundled PostgreSQL:
postgresql['enable'] = true postgresql['enable'] = true
``` ```
1. Edit `/etc/gitlab/gitlab.rb` to use the bundled PostgreSQL. Please check 1. Edit `/etc/gitlab/gitlab.rb` to use the bundled PostgreSQL. Review all of the
all the settings beginning with `db_`, such as `gitlab_rails['db_adapter']` settings beginning with `db_` (such as `gitlab_rails['db_adapter']`). To use
and alike. You could just comment all of them out so that we'll just use the default values, you can comment all of them out.
the defaults.
1. [Reconfigure GitLab](../administration/restart_gitlab.md#omnibus-gitlab-reconfigure) 1. [Reconfigure GitLab](../administration/restart_gitlab.md#omnibus-gitlab-reconfigure)
for the changes to take effect. for the changes to take effect.
1. Start Unicorn and PostgreSQL so that we can prepare the schema: 1. Start Unicorn and PostgreSQL so that we can prepare the schema:
```shell ```shell
...@@ -110,9 +110,9 @@ need to enable the bundled PostgreSQL: ...@@ -110,9 +110,9 @@ need to enable the bundled PostgreSQL:
sudo gitlab-ctl stop unicorn sudo gitlab-ctl stop unicorn
``` ```
After these steps, you'll have a fresh PostgreSQL database with up-to-date schema. After these steps, you have a fresh PostgreSQL database with up-to-date schema.
Next, we'll use `pgloader` to migrate the data from the old MySQL database to the Next, use `pgloader` to migrate the data from the old MySQL database to the
new PostgreSQL one: new PostgreSQL one:
1. Save the following snippet in a `commands.load` file, and edit with your 1. Save the following snippet in a `commands.load` file, and edit with your
...@@ -178,7 +178,7 @@ You can now verify that everything works as expected by visiting GitLab. ...@@ -178,7 +178,7 @@ You can now verify that everything works as expected by visiting GitLab.
## Source installations ## Source installations
For installations from source that use MySQL, you'll first need to For installations from source that use MySQL, you must first
[install PostgreSQL and create a database](../install/installation.md#6-database). [install PostgreSQL and create a database](../install/installation.md#6-database).
After the database is created, go on with the following steps: After the database is created, go on with the following steps:
...@@ -211,9 +211,9 @@ After the database is created, go on with the following steps: ...@@ -211,9 +211,9 @@ After the database is created, go on with the following steps:
sudo -u git -H bundle exec rake db:create db:migrate RAILS_ENV=production sudo -u git -H bundle exec rake db:create db:migrate RAILS_ENV=production
``` ```
After these steps, you'll have a fresh PostgreSQL database with up-to-date schema. After these steps, you have a fresh PostgreSQL database with up-to-date schema.
Next, we'll use `pgloader` to migrate the data from the old MySQL database to the Next, use `pgloader` to migrate the data from the old MySQL database to the
new PostgreSQL one: new PostgreSQL one:
1. Save the following snippet in a `commands.load` file, and edit with your 1. Save the following snippet in a `commands.load` file, and edit with your
......
...@@ -65,7 +65,7 @@ sudo gitlab-rake gitlab:db:mark_migration_complete[20151103134857] ...@@ -65,7 +65,7 @@ sudo gitlab-rake gitlab:db:mark_migration_complete[20151103134857]
``` ```
Once the migration is successfully marked, run the Rake `db:migrate` task again. Once the migration is successfully marked, run the Rake `db:migrate` task again.
You will likely have to repeat this process several times until all failed You might need to repeat this process several times until all failed
migrations are marked complete. migrations are marked complete.
### GitLab < 8.6 ### GitLab < 8.6
...@@ -86,5 +86,5 @@ exit ...@@ -86,5 +86,5 @@ exit
``` ```
Once the migration is successfully marked, run the Rake `db:migrate` task again. Once the migration is successfully marked, run the Rake `db:migrate` task again.
You will likely have to repeat this process several times until all failed You might need to repeat this process several times until all failed
migrations are marked complete. migrations are marked complete.
...@@ -128,7 +128,7 @@ sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production ...@@ -128,7 +128,7 @@ sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
Certain versions of GitLab may require you to perform additional steps when Certain versions of GitLab may require you to perform additional steps when
upgrading from Community Edition to Enterprise Edition. Should such steps be upgrading from Community Edition to Enterprise Edition. Should such steps be
necessary, they will listed per version below. necessary, they are listed per version below.
<!-- <!--
Example: Example:
......
...@@ -8,7 +8,7 @@ comments: false ...@@ -8,7 +8,7 @@ comments: false
# Upgrading Community Edition and Enterprise Edition from source # Upgrading Community Edition and Enterprise Edition from source
NOTE: NOTE:
Users wishing to upgrade to 12.0.0 will have to take some extra steps. See the Users wishing to upgrade to 12.0.0 must take some extra steps. See the
version specific upgrade instructions for 12.0.0 for more details. version specific upgrade instructions for 12.0.0 for more details.
Make sure you view this update guide from the branch (version) of GitLab you Make sure you view this update guide from the branch (version) of GitLab you
...@@ -284,12 +284,12 @@ longer handles setting it. ...@@ -284,12 +284,12 @@ longer handles setting it.
If you are using Apache instead of NGINX see the updated [Apache templates](https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache). If you are using Apache instead of NGINX see the updated [Apache templates](https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache).
Also note that because Apache does not support upstreams behind Unix sockets you Also note that because Apache does not support upstreams behind Unix sockets you
will need to let GitLab Workhorse listen on a TCP port. You can do this must let GitLab Workhorse listen on a TCP port. You can do this
via [`/etc/default/gitlab`](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/support/init.d/gitlab.default.example#L38). via [`/etc/default/gitlab`](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/support/init.d/gitlab.default.example#L38).
#### SMTP configuration #### SMTP configuration
If you're installing from source and use SMTP to deliver mail, you will need to If you're installing from source and use SMTP to deliver mail, you must
add the following line to `config/initializers/smtp_settings.rb`: add the following line to `config/initializers/smtp_settings.rb`:
```ruby ```ruby
......
...@@ -9,7 +9,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w ...@@ -9,7 +9,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
Two-factor authentication (2FA) provides an additional level of security to your Two-factor authentication (2FA) provides an additional level of security to your
GitLab account. After being enabled, in addition to supplying your username and GitLab account. After being enabled, in addition to supplying your username and
password to sign in, you'll be prompted for a code generated by your one-time password to sign in, you are prompted for a code generated by your one-time
password authenticator (for example, a password manager on one of your devices). password authenticator (for example, a password manager on one of your devices).
By enabling 2FA, the only way someone other than you can sign in to your account By enabling 2FA, the only way someone other than you can sign in to your account
...@@ -22,20 +22,21 @@ TIP: **Tip:** ...@@ -22,20 +22,21 @@ TIP: **Tip:**
When you enable 2FA, don't forget to back up your [recovery codes](#recovery-codes)! When you enable 2FA, don't forget to back up your [recovery codes](#recovery-codes)!
In addition to time-based one time passwords (TOTP), GitLab supports U2F In addition to time-based one time passwords (TOTP), GitLab supports U2F
(universal 2nd factor) and WebAuthn (experimental) devices as the second factor of authentication. Once (universal 2nd factor) and WebAuthn (experimental) devices as the second factor
enabled, in addition to supplying your username and password to log in, you'll of authentication. After being enabled, in addition to supplying your username
be prompted to activate your U2F / WebAuthn device (usually by pressing a button on it), and password to sign in, you're prompted to activate your U2F / WebAuthn device
and it will perform secure authentication on your behalf. (usually by pressing a button on it) which performs secure authentication on
your behalf.
It is highly recommended that you set up 2FA with both a It's highly recommended that you set up 2FA with both a [one-time password authenticator](#one-time-password)
[one-time password authenticator](#one-time-password) or use [FortiAuthenticator](#one-time-password-via-fortiauthenticator) or use [FortiAuthenticator](#one-time-password-via-fortiauthenticator) and a
and a [U2F device](#u2f-device) or a [WebAuthn device](#webauthn-device), so you can still access your account [U2F device](#u2f-device) or a [WebAuthn device](#webauthn-device), so you can
if you lose your U2F / WebAuthn device. still access your account if you lose your U2F / WebAuthn device.
## Enabling 2FA ## Enabling 2FA
There are multiple ways to enable two-factor authentication: via a one time password authenticator There are multiple ways to enable two-factor authentication: by using a one-time
or a U2F / WebAuthn device. password authenticator or a U2F / WebAuthn device.
### One-time password ### One-time password
...@@ -62,8 +63,8 @@ To enable 2FA: ...@@ -62,8 +63,8 @@ To enable 2FA:
code** field. code** field.
1. Select **Submit**. 1. Select **Submit**.
If the pin you entered was correct, you'll see a message indicating that If the pin you entered was correct, a message displays indicating that
two-factor authentication has been enabled, and you'll be presented with a list two-factor authentication has been enabled, and you're shown a list
of [recovery codes](#recovery-codes). Be sure to download them and keep them of [recovery codes](#recovery-codes). Be sure to download them and keep them
in a safe place. in a safe place.
...@@ -77,7 +78,7 @@ You can use FortiAuthenticator as an OTP provider in GitLab. Users must exist in ...@@ -77,7 +78,7 @@ You can use FortiAuthenticator as an OTP provider in GitLab. Users must exist in
both FortiAuthenticator and GitLab with the exact same username, and users must both FortiAuthenticator and GitLab with the exact same username, and users must
have FortiToken configured in FortiAuthenticator. have FortiToken configured in FortiAuthenticator.
You'll also need a username and access token for FortiAuthenticator. The You need a username and access token for FortiAuthenticator. The
`access_token` in the code samples shown below is the FortAuthenticator access `access_token` in the code samples shown below is the FortAuthenticator access
key. To get the token, see the `REST API Solution Guide` at key. To get the token, see the `REST API Solution Guide` at
[`Fortinet Document Library`](https://docs.fortinet.com/document/fortiauthenticator/6.2.0/rest-api-solution-guide/158294/the-fortiauthenticator-api). [`Fortinet Document Library`](https://docs.fortinet.com/document/fortiauthenticator/6.2.0/rest-api-solution-guide/158294/the-fortiauthenticator-api).
...@@ -170,9 +171,9 @@ To set up 2FA with a U2F device: ...@@ -170,9 +171,9 @@ To set up 2FA with a U2F device:
1. Click **Enable Two-Factor Authentication**. 1. Click **Enable Two-Factor Authentication**.
1. Connect your U2F device. 1. Connect your U2F device.
1. Click on **Set up New U2F Device**. 1. Click on **Set up New U2F Device**.
1. A light will start blinking on your device. Activate it by pressing its button. 1. A light begins blinking on your device. Activate it by pressing its button.
You will see a message indicating that your device was successfully set up. A message displays, indicating that your device was successfully set up.
Click on **Register U2F Device** to complete the process. Click on **Register U2F Device** to complete the process.
### WebAuthn device ### WebAuthn device
...@@ -208,7 +209,7 @@ To set up 2FA with a WebAuthn compatible device: ...@@ -208,7 +209,7 @@ To set up 2FA with a WebAuthn compatible device:
1. Select **Set up New WebAuthn Device**. 1. Select **Set up New WebAuthn Device**.
1. Depending on your device, you might need to press a button or touch a sensor. 1. Depending on your device, you might need to press a button or touch a sensor.
You will see a message indicating that your device was successfully set up. A message displays, indicating that your device was successfully set up.
Recovery codes are not generated for WebAuthn devices. Recovery codes are not generated for WebAuthn devices.
## Recovery codes ## Recovery codes
...@@ -219,12 +220,12 @@ Recovery codes are not generated for U2F / WebAuthn devices. ...@@ -219,12 +220,12 @@ Recovery codes are not generated for U2F / WebAuthn devices.
WARNING: WARNING:
Each code can be used only once to log in to your account. Each code can be used only once to log in to your account.
Immediately after successfully enabling two-factor authentication, you'll be Immediately after successfully enabling two-factor authentication, you're
prompted to download a set of generated recovery codes. Should you ever lose access prompted to download a set of generated recovery codes. Should you ever lose access
to your one-time password authenticator, you can use one of these recovery codes to log in to to your one-time password authenticator, you can use one of these recovery codes to log in to
your account. We suggest copying and printing them, or downloading them using your account. We suggest copying and printing them, or downloading them using
the **Download codes** button for storage in a safe place. If you choose to the **Download codes** button for storage in a safe place. If you choose to
download them, the file will be called `gitlab-recovery-codes.txt`. download them, the file is called `gitlab-recovery-codes.txt`.
If you lose the recovery codes or just want to generate new ones, you can do so If you lose the recovery codes or just want to generate new ones, you can do so
from the [two-factor authentication account settings page](#regenerate-2fa-recovery-codes) or from the [two-factor authentication account settings page](#regenerate-2fa-recovery-codes) or
...@@ -233,8 +234,8 @@ from the [two-factor authentication account settings page](#regenerate-2fa-recov ...@@ -233,8 +234,8 @@ from the [two-factor authentication account settings page](#regenerate-2fa-recov
## Logging in with 2FA Enabled ## Logging in with 2FA Enabled
Logging in with 2FA enabled is only slightly different than a normal login. Logging in with 2FA enabled is only slightly different than a normal login.
Enter your username and password credentials as you normally would, and you'll Enter your username and password credentials as you normally would, and you're
be presented with a second prompt, depending on which type of 2FA you've enabled. presented with a second prompt, depending on which type of 2FA you've enabled.
### Log in via a one-time password ### Log in via a one-time password
...@@ -246,19 +247,19 @@ recovery code to log in. ...@@ -246,19 +247,19 @@ recovery code to log in.
To log in via a U2F device: To log in via a U2F device:
1. Click **Login via U2F Device**. 1. Click **Login via U2F Device**.
1. A light will start blinking on your device. Activate it by touching/pressing 1. A light begins blinking on your device. Activate it by touching/pressing
its button. its button.
You will see a message indicating that your device responded to the authentication A message displays, indicating that your device responded to the authentication
request and you will be automatically logged in. request, and you're automatically logged in.
### Log in via WebAuthn device ### Log in via WebAuthn device
In supported browsers you should be automatically prompted to activate your WebAuthn device In supported browsers you should be automatically prompted to activate your WebAuthn device
(e.g. by touching/pressing its button) after entering your credentials. (e.g. by touching/pressing its button) after entering your credentials.
You will see a message indicating that your device responded to the authentication A message displays, indicating that your device responded to the authentication
request and you will be automatically logged in. request and you're automatically logged in.
## Disabling 2FA ## Disabling 2FA
...@@ -269,7 +270,7 @@ If you ever need to disable 2FA: ...@@ -269,7 +270,7 @@ If you ever need to disable 2FA:
1. Go to **Account**. 1. Go to **Account**.
1. Click **Disable**, under **Two-Factor Authentication**. 1. Click **Disable**, under **Two-Factor Authentication**.
This will clear all your two-factor authentication registrations, including mobile This clears all your two-factor authentication registrations, including mobile
applications and U2F / WebAuthn devices. applications and U2F / WebAuthn devices.
## Personal access tokens ## Personal access tokens
...@@ -312,7 +313,7 @@ a new set of recovery codes with SSH: ...@@ -312,7 +313,7 @@ a new set of recovery codes with SSH:
ssh git@gitlab.example.com 2fa_recovery_codes ssh git@gitlab.example.com 2fa_recovery_codes
``` ```
1. You will then be prompted to confirm that you want to generate new codes. 1. You are prompted to confirm that you want to generate new codes.
Continuing this process invalidates previously saved codes: Continuing this process invalidates previously saved codes:
```shell ```shell
...@@ -358,13 +359,13 @@ To regenerate 2FA recovery codes, you need access to a desktop browser: ...@@ -358,13 +359,13 @@ To regenerate 2FA recovery codes, you need access to a desktop browser:
1. In the **Register Two-Factor Authenticator** pane, click **Regenerate recovery codes**. 1. In the **Register Two-Factor Authenticator** pane, click **Regenerate recovery codes**.
NOTE: NOTE:
If you regenerate 2FA recovery codes, save them. You won't be able to use any previously created 2FA codes. If you regenerate 2FA recovery codes, save them. You can't use any previously created 2FA codes.
### Ask a GitLab administrator to disable two-factor authentication on your account ### Ask a GitLab administrator to disable two-factor authentication on your account
If you cannot use a saved recovery code or generate new recovery codes, ask a If you cannot use a saved recovery code or generate new recovery codes, ask a
GitLab global administrator to disable two-factor authentication for your GitLab global administrator to disable two-factor authentication for your
account. This will temporarily leave your account in a less secure state. account. This temporarily leaves your account in a less secure state.
Sign in and re-enable two-factor authentication as soon as possible. Sign in and re-enable two-factor authentication as soon as possible.
## Note to GitLab administrators ## Note to GitLab administrators
......
...@@ -305,7 +305,7 @@ you are asked to sign in again to verify your identity for security reasons. ...@@ -305,7 +305,7 @@ you are asked to sign in again to verify your identity for security reasons.
NOTE: NOTE:
When any session is signed out, or when a session is revoked When any session is signed out, or when a session is revoked
via [Active Sessions](active_sessions.md), all **Remember me** tokens are revoked. via [Active Sessions](active_sessions.md), all **Remember me** tokens are revoked.
While other sessions will remain active, the **Remember me** feature will not restore While other sessions remain active, the **Remember me** feature doesn't restore
a session if the browser is closed or the existing session expires. a session if the browser is closed or the existing session expires.
### Increased sign-in time ### Increased sign-in time
......
...@@ -21,7 +21,7 @@ You receive notifications for one of the following reasons: ...@@ -21,7 +21,7 @@ You receive notifications for one of the following reasons:
While notifications are enabled, you receive notification of actions occurring in that issue, merge request, or epic. While notifications are enabled, you receive notification of actions occurring in that issue, merge request, or epic.
NOTE: NOTE:
Notifications can be blocked by an admin, preventing them from being sent. Notifications can be blocked by an administrator, preventing them from being sent.
## Tuning your notifications ## Tuning your notifications
......
...@@ -59,9 +59,9 @@ For this association to succeed, each GitHub author and assignee in the reposito ...@@ -59,9 +59,9 @@ For this association to succeed, each GitHub author and assignee in the reposito
must meet one of the following conditions prior to the import: must meet one of the following conditions prior to the import:
- Have previously logged in to a GitLab account using the GitHub icon. - Have previously logged in to a GitLab account using the GitHub icon.
- Have a GitHub account with a - Have a GitHub account with a publicly visible
[primary email address](https://docs.github.com/en/free-pro-team@latest/github/setting-up-and-managing-your-github-user-account/setting-your-commit-email-address) [primary email address](https://docs.github.com/en/free-pro-team@latest/rest/reference/users#get-a-user)
that matches their GitLab account's email address. on their profile that matches their GitLab account's email address.
If a user referenced in the project is not found in GitLab's database, the project creator (typically the user If a user referenced in the project is not found in GitLab's database, the project creator (typically the user
that initiated the import process) is set as the author/assignee, but a note on the issue mentioning the original that initiated the import process) is set as the author/assignee, but a note on the issue mentioning the original
...@@ -89,7 +89,7 @@ Before you begin, ensure that any GitHub users who you want to map to GitLab use ...@@ -89,7 +89,7 @@ Before you begin, ensure that any GitHub users who you want to map to GitLab use
- A GitLab account that has logged in using the GitHub icon - A GitLab account that has logged in using the GitHub icon
\- or - \- or -
- A GitLab account with an email address that matches the [public email address](https://docs.github.com/en/free-pro-team@latest/github/setting-up-and-managing-your-github-user-account/setting-your-commit-email-address) of the GitHub user - A GitLab account with an email address that matches the [publicly visible email address](https://docs.github.com/en/free-pro-team@latest/rest/reference/users#get-a-user) in the profile of the GitHub user
User-matching attempts occur in that order, and if a user is not identified either way, the activity is associated with User-matching attempts occur in that order, and if a user is not identified either way, the activity is associated with
the user account that is performing the import. the user account that is performing the import.
......
...@@ -30,7 +30,7 @@ import ProfileSelectorSummaryCell from './profile_selector/summary_cell.vue'; ...@@ -30,7 +30,7 @@ import ProfileSelectorSummaryCell from './profile_selector/summary_cell.vue';
import ScannerProfileSelector from './profile_selector/scanner_profile_selector.vue'; import ScannerProfileSelector from './profile_selector/scanner_profile_selector.vue';
import SiteProfileSelector from './profile_selector/site_profile_selector.vue'; import SiteProfileSelector from './profile_selector/site_profile_selector.vue';
const createProfilesApolloOptions = (name, { fetchQuery, fetchError }) => ({ const createProfilesApolloOptions = (name, field, { fetchQuery, fetchError }) => ({
query: fetchQuery, query: fetchQuery,
variables() { variables() {
return { return {
...@@ -39,6 +39,9 @@ const createProfilesApolloOptions = (name, { fetchQuery, fetchError }) => ({ ...@@ -39,6 +39,9 @@ const createProfilesApolloOptions = (name, { fetchQuery, fetchError }) => ({
}, },
update(data) { update(data) {
const edges = data?.project?.[name]?.edges ?? []; const edges = data?.project?.[name]?.edges ?? [];
if (edges.length === 1) {
this[field] = edges[0].node.id;
}
return edges.map(({ node }) => node); return edges.map(({ node }) => node);
}, },
error(e) { error(e) {
...@@ -66,8 +69,16 @@ export default { ...@@ -66,8 +69,16 @@ export default {
}, },
mixins: [glFeatureFlagsMixin()], mixins: [glFeatureFlagsMixin()],
apollo: { apollo: {
scannerProfiles: createProfilesApolloOptions('scannerProfiles', SCANNER_PROFILES_QUERY), scannerProfiles: createProfilesApolloOptions(
siteProfiles: createProfilesApolloOptions('siteProfiles', SITE_PROFILES_QUERY), 'scannerProfiles',
'selectedScannerProfileId',
SCANNER_PROFILES_QUERY,
),
siteProfiles: createProfilesApolloOptions(
'siteProfiles',
'selectedSiteProfileId',
SITE_PROFILES_QUERY,
),
}, },
props: { props: {
helpPagePath: { helpPagePath: {
...@@ -104,8 +115,8 @@ export default { ...@@ -104,8 +115,8 @@ export default {
return { return {
scannerProfiles: [], scannerProfiles: [],
siteProfiles: [], siteProfiles: [],
selectedScannerProfile: null, selectedScannerProfileId: null,
selectedSiteProfile: null, selectedSiteProfileId: null,
loading: false, loading: false,
errorType: null, errorType: null,
errors: [], errors: [],
...@@ -113,6 +124,16 @@ export default { ...@@ -113,6 +124,16 @@ export default {
}; };
}, },
computed: { computed: {
selectedScannerProfile() {
return this.selectedScannerProfileId
? this.scannerProfiles.find(({ id }) => id === this.selectedScannerProfileId)
: null;
},
selectedSiteProfile() {
return this.selectedSiteProfileId
? this.siteProfiles.find(({ id }) => id === this.selectedSiteProfileId)
: null;
},
errorMessage() { errorMessage() {
return ERROR_MESSAGES[this.errorType] || null; return ERROR_MESSAGES[this.errorType] || null;
}, },
...@@ -238,37 +259,37 @@ export default { ...@@ -238,37 +259,37 @@ export default {
</template> </template>
<template v-else-if="!failedToLoadProfiles"> <template v-else-if="!failedToLoadProfiles">
<scanner-profile-selector <scanner-profile-selector
v-model="selectedScannerProfile" v-model="selectedScannerProfileId"
class="gl-mb-5" class="gl-mb-5"
:profiles="scannerProfiles" :profiles="scannerProfiles"
> >
<template #summary="{ profile }"> <template v-if="selectedScannerProfile" #summary>
<div class="row"> <div class="row">
<profile-selector-summary-cell <profile-selector-summary-cell
:class="{ 'gl-text-red-500': hasProfilesConflict }" :class="{ 'gl-text-red-500': hasProfilesConflict }"
:label="s__('DastProfiles|Scan mode')" :label="s__('DastProfiles|Scan mode')"
:value="$options.SCAN_TYPE_LABEL[profile.scanType]" :value="$options.SCAN_TYPE_LABEL[selectedScannerProfile.scanType]"
/> />
</div> </div>
<div class="row"> <div class="row">
<profile-selector-summary-cell <profile-selector-summary-cell
:label="s__('DastProfiles|Spider timeout')" :label="s__('DastProfiles|Spider timeout')"
:value="n__('%d minute', '%d minutes', profile.spiderTimeout)" :value="n__('%d minute', '%d minutes', selectedScannerProfile.spiderTimeout)"
/> />
<profile-selector-summary-cell <profile-selector-summary-cell
:label="s__('DastProfiles|Target timeout')" :label="s__('DastProfiles|Target timeout')"
:value="n__('%d second', '%d seconds', profile.targetTimeout)" :value="n__('%d second', '%d seconds', selectedScannerProfile.targetTimeout)"
/> />
</div> </div>
<div class="row"> <div class="row">
<profile-selector-summary-cell <profile-selector-summary-cell
:label="s__('DastProfiles|AJAX spider')" :label="s__('DastProfiles|AJAX spider')"
:value="profile.useAjaxSpider ? __('On') : __('Off')" :value="selectedScannerProfile.useAjaxSpider ? __('On') : __('Off')"
/> />
<profile-selector-summary-cell <profile-selector-summary-cell
:label="s__('DastProfiles|Debug messages')" :label="s__('DastProfiles|Debug messages')"
:value=" :value="
profile.showDebugMessages selectedScannerProfile.showDebugMessages
? s__('DastProfiles|Show debug messages') ? s__('DastProfiles|Show debug messages')
: s__('DastProfiles|Hide debug messages') : s__('DastProfiles|Hide debug messages')
" "
...@@ -276,13 +297,17 @@ export default { ...@@ -276,13 +297,17 @@ export default {
</div> </div>
</template> </template>
</scanner-profile-selector> </scanner-profile-selector>
<site-profile-selector v-model="selectedSiteProfile" class="gl-mb-5" :profiles="siteProfiles"> <site-profile-selector
<template #summary="{ profile }"> v-model="selectedSiteProfileId"
class="gl-mb-5"
:profiles="siteProfiles"
>
<template v-if="selectedSiteProfile" #summary>
<div class="row"> <div class="row">
<profile-selector-summary-cell <profile-selector-summary-cell
:class="{ 'gl-text-red-500': hasProfilesConflict }" :class="{ 'gl-text-red-500': hasProfilesConflict }"
:label="s__('DastProfiles|Target URL')" :label="s__('DastProfiles|Target URL')"
:value="profile.targetUrl" :value="selectedSiteProfile.targetUrl"
/> />
</div> </div>
</template> </template>
......
...@@ -25,14 +25,14 @@ export default { ...@@ -25,14 +25,14 @@ export default {
default: () => [], default: () => [],
}, },
value: { value: {
type: Object, type: String,
required: false, required: false,
default: null, default: null,
}, },
}, },
methods: { computed: {
isChecked({ id }) { selectedProfile() {
return this.value?.id === id; return this.value ? this.profiles.find(({ id }) => this.value === id) : null;
}, },
}, },
}; };
...@@ -67,7 +67,9 @@ export default { ...@@ -67,7 +67,9 @@ export default {
</template> </template>
<gl-dropdown <gl-dropdown
:text=" :text="
value ? value.dropdownLabel : s__('OnDemandScans|Select one of the existing profiles') selectedProfile
? selectedProfile.dropdownLabel
: s__('OnDemandScans|Select one of the existing profiles')
" "
class="mw-460" class="mw-460"
data-testid="profiles-dropdown" data-testid="profiles-dropdown"
...@@ -75,9 +77,9 @@ export default { ...@@ -75,9 +77,9 @@ export default {
<gl-dropdown-item <gl-dropdown-item
v-for="profile in profiles" v-for="profile in profiles"
:key="profile.id" :key="profile.id"
:is-checked="isChecked(profile)" :is-checked="value === profile.id"
is-check-item is-check-item
@click="$emit('input', profile)" @click="$emit('input', profile.id)"
> >
{{ profile.profileName }} {{ profile.profileName }}
</gl-dropdown-item> </gl-dropdown-item>
...@@ -87,7 +89,7 @@ export default { ...@@ -87,7 +89,7 @@ export default {
data-testid="selected-profile-summary" data-testid="selected-profile-summary"
class="gl-mt-6 gl-pt-6 gl-border-t-solid gl-border-gray-100 gl-border-t-1" class="gl-mt-6 gl-pt-6 gl-border-t-solid gl-border-gray-100 gl-border-t-1"
> >
<slot name="summary" :profile="value"></slot> <slot name="summary"></slot>
</div> </div>
</gl-form-group> </gl-form-group>
<template v-else> <template v-else>
......
...@@ -57,8 +57,8 @@ export default { ...@@ -57,8 +57,8 @@ export default {
) )
}}</template> }}</template>
<template #new-profile>{{ s__('OnDemandScans|Create a new scanner profile') }}</template> <template #new-profile>{{ s__('OnDemandScans|Create a new scanner profile') }}</template>
<template #summary="{ profile }"> <template #summary>
<slot name="summary" :profile="profile"></slot> <slot name="summary"></slot>
</template> </template>
</profile-selector> </profile-selector>
</template> </template>
...@@ -60,8 +60,8 @@ export default { ...@@ -60,8 +60,8 @@ export default {
) )
}}</template> }}</template>
<template #new-profile>{{ s__('OnDemandScans|Create a new site profile') }}</template> <template #new-profile>{{ s__('OnDemandScans|Create a new site profile') }}</template>
<template #summary="{ profile }"> <template #summary>
<slot name="summary" :profile="profile"></slot> <slot name="summary"></slot>
</template> </template>
</profile-selector> </profile-selector>
</template> </template>
...@@ -65,6 +65,7 @@ module EE ...@@ -65,6 +65,7 @@ module EE
override :check_access override :check_access
def check_access(user) def check_access(user)
return false unless user
return true if user.admin? return true if user.admin?
return user.id == self.user_id if self.user.present? return user.id == self.user_id if self.user.present?
return group.users.exists?(user.id) if self.group.present? return group.users.exists?(user.id) if self.group.present?
......
---
title: 'On-demand scans: automatically select DAST profile when only one is available'
merge_request: 48940
author:
type: changed
import { GlForm, GlSkeletonLoader } from '@gitlab/ui'; import { GlForm, GlSkeletonLoader } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils'; import { shallowMount, mount, createLocalVue } from '@vue/test-utils';
import { merge } from 'lodash'; import { merge } from 'lodash';
import VueApollo from 'vue-apollo';
import createApolloProvider from 'helpers/mock_apollo_helper';
import OnDemandScansForm from 'ee/on_demand_scans/components/on_demand_scans_form.vue'; import OnDemandScansForm from 'ee/on_demand_scans/components/on_demand_scans_form.vue';
import ScannerProfileSelector from 'ee/on_demand_scans/components/profile_selector/scanner_profile_selector.vue'; import ScannerProfileSelector from 'ee/on_demand_scans/components/profile_selector/scanner_profile_selector.vue';
import SiteProfileSelector from 'ee/on_demand_scans/components/profile_selector/site_profile_selector.vue'; import SiteProfileSelector from 'ee/on_demand_scans/components/profile_selector/site_profile_selector.vue';
import dastOnDemandScanCreate from 'ee/on_demand_scans/graphql/dast_on_demand_scan_create.mutation.graphql'; import dastOnDemandScanCreate from 'ee/on_demand_scans/graphql/dast_on_demand_scan_create.mutation.graphql';
import dastScannerProfilesQuery from 'ee/security_configuration/dast_profiles/graphql/dast_scanner_profiles.query.graphql';
import dastSiteProfilesQuery from 'ee/security_configuration/dast_profiles/graphql/dast_site_profiles.query.graphql';
import * as responses from '../mocks/apollo_mocks';
import { scannerProfiles, siteProfiles } from '../mocks/mock_data';
import { redirectTo } from '~/lib/utils/url_utility'; import { redirectTo } from '~/lib/utils/url_utility';
import { scannerProfiles, siteProfiles } from '../mock_data';
const helpPagePath = '/application_security/dast/index#on-demand-scans'; const helpPagePath = '/application_security/dast/index#on-demand-scans';
const projectPath = 'group/project'; const projectPath = 'group/project';
...@@ -22,17 +27,6 @@ const defaultProps = { ...@@ -22,17 +27,6 @@ const defaultProps = {
defaultBranch, defaultBranch,
}; };
const defaultMocks = {
$apollo: {
mutate: jest.fn(),
queries: {
scannerProfiles: {},
siteProfiles: {},
},
addSmartQuery: jest.fn(),
},
};
const pipelineUrl = `/${projectPath}/pipelines/123`; const pipelineUrl = `/${projectPath}/pipelines/123`;
const [passiveScannerProfile, activeScannerProfile] = scannerProfiles; const [passiveScannerProfile, activeScannerProfile] = scannerProfiles;
const [nonValidatedSiteProfile, validatedSiteProfile] = siteProfiles; const [nonValidatedSiteProfile, validatedSiteProfile] = siteProfiles;
...@@ -43,7 +37,9 @@ jest.mock('~/lib/utils/url_utility', () => ({ ...@@ -43,7 +37,9 @@ jest.mock('~/lib/utils/url_utility', () => ({
})); }));
describe('OnDemandScansForm', () => { describe('OnDemandScansForm', () => {
let localVue;
let subject; let subject;
let requestHandlers;
const findForm = () => subject.find(GlForm); const findForm = () => subject.find(GlForm);
const findByTestId = testId => subject.find(`[data-testid="${testId}"]`); const findByTestId = testId => subject.find(`[data-testid="${testId}"]`);
...@@ -52,13 +48,44 @@ describe('OnDemandScansForm', () => { ...@@ -52,13 +48,44 @@ describe('OnDemandScansForm', () => {
const findSubmitButton = () => findByTestId('on-demand-scan-submit-button'); const findSubmitButton = () => findByTestId('on-demand-scan-submit-button');
const setValidFormData = () => { const setValidFormData = () => {
subject.find(ScannerProfileSelector).vm.$emit('input', passiveScannerProfile); subject.find(ScannerProfileSelector).vm.$emit('input', passiveScannerProfile.id);
subject.find(SiteProfileSelector).vm.$emit('input', nonValidatedSiteProfile); subject.find(SiteProfileSelector).vm.$emit('input', nonValidatedSiteProfile.id);
return subject.vm.$nextTick(); return subject.vm.$nextTick();
}; };
const submitForm = () => findForm().vm.$emit('submit', { preventDefault: () => {} }); const submitForm = () => findForm().vm.$emit('submit', { preventDefault: () => {} });
const subjectMounterFactory = (mountFn = shallowMount) => (options = {}) => { const createMockApolloProvider = handlers => {
localVue.use(VueApollo);
requestHandlers = {
dastScannerProfiles: jest.fn().mockResolvedValue(responses.dastScannerProfiles()),
dastSiteProfiles: jest.fn().mockResolvedValue(responses.dastSiteProfiles()),
...handlers,
};
return createApolloProvider([
[dastScannerProfilesQuery, requestHandlers.dastScannerProfiles],
[dastSiteProfilesQuery, requestHandlers.dastSiteProfiles],
]);
};
const subjectMounterFactory = (mountFn = shallowMount) => (options = {}, withHandlers) => {
localVue = createLocalVue();
let defaultMocks = {
$apollo: {
mutate: jest.fn(),
queries: {
scannerProfiles: {},
siteProfiles: {},
},
addSmartQuery: jest.fn(),
},
};
let apolloProvider;
if (withHandlers) {
apolloProvider = createMockApolloProvider(withHandlers);
defaultMocks = {};
}
subject = mountFn( subject = mountFn(
OnDemandScansForm, OnDemandScansForm,
merge( merge(
...@@ -76,7 +103,7 @@ describe('OnDemandScansForm', () => { ...@@ -76,7 +103,7 @@ describe('OnDemandScansForm', () => {
}, },
}, },
}, },
options, { ...options, localVue, apolloProvider },
{ {
data() { data() {
return { ...options.data }; return { ...options.data };
...@@ -243,8 +270,8 @@ describe('OnDemandScansForm', () => { ...@@ -243,8 +270,8 @@ describe('OnDemandScansForm', () => {
'profiles conflict prevention', 'profiles conflict prevention',
({ description, selectedScannerProfile, selectedSiteProfile, hasConflict }) => { ({ description, selectedScannerProfile, selectedSiteProfile, hasConflict }) => {
const setFormData = () => { const setFormData = () => {
subject.find(ScannerProfileSelector).vm.$emit('input', selectedScannerProfile); subject.find(ScannerProfileSelector).vm.$emit('input', selectedScannerProfile.id);
subject.find(SiteProfileSelector).vm.$emit('input', selectedSiteProfile); subject.find(SiteProfileSelector).vm.$emit('input', selectedSiteProfile.id);
return subject.vm.$nextTick(); return subject.vm.$nextTick();
}; };
...@@ -253,7 +280,12 @@ describe('OnDemandScansForm', () => { ...@@ -253,7 +280,12 @@ describe('OnDemandScansForm', () => {
? `warns about conflicting profiles when user selects ${description}` ? `warns about conflicting profiles when user selects ${description}`
: `does not report any conflict when user selects ${description}`, : `does not report any conflict when user selects ${description}`,
async () => { async () => {
mountShallowSubject(); mountShallowSubject({
data: {
scannerProfiles,
siteProfiles,
},
});
await setFormData(); await setFormData();
expect(findProfilesConflictAlert().exists()).toBe(hasConflict); expect(findProfilesConflictAlert().exists()).toBe(hasConflict);
...@@ -269,6 +301,10 @@ describe('OnDemandScansForm', () => { ...@@ -269,6 +301,10 @@ describe('OnDemandScansForm', () => {
securityOnDemandScansSiteValidation: false, securityOnDemandScansSiteValidation: false,
}, },
}, },
data: {
scannerProfiles,
siteProfiles,
},
}); });
return setFormData(); return setFormData();
}); });
...@@ -280,4 +316,25 @@ describe('OnDemandScansForm', () => { ...@@ -280,4 +316,25 @@ describe('OnDemandScansForm', () => {
}); });
}, },
); );
describe.each`
profileType | query | selector | profiles
${'scanner'} | ${'dastScannerProfiles'} | ${ScannerProfileSelector} | ${scannerProfiles}
${'site'} | ${'dastSiteProfiles'} | ${SiteProfileSelector} | ${siteProfiles}
`('when there is a single $profileType profile', ({ query, selector, profiles }) => {
const [profile] = profiles;
beforeEach(() => {
mountShallowSubject(
{},
{
[query]: jest.fn().mockResolvedValue(responses[query]([profile])),
},
);
});
it('automatically selects the only available profile', () => {
expect(subject.find(selector).attributes('value')).toBe(profile.id);
});
});
}); });
...@@ -4,7 +4,7 @@ exports[`OnDemandScansScannerProfileSelector renders properly with profiles 1`] ...@@ -4,7 +4,7 @@ exports[`OnDemandScansScannerProfileSelector renders properly with profiles 1`]
<div <div
class="gl-card" class="gl-card"
data-foo="bar" data-foo="bar"
value="[object Object]" value="gid://gitlab/DastScannerProfile/1"
> >
<div <div
class="gl-card-header" class="gl-card-header"
......
...@@ -4,7 +4,7 @@ exports[`OnDemandScansSiteProfileSelector renders properly with profiles 1`] = ` ...@@ -4,7 +4,7 @@ exports[`OnDemandScansSiteProfileSelector renders properly with profiles 1`] = `
<div <div
class="gl-card" class="gl-card"
data-foo="bar" data-foo="bar"
value="[object Object]" value="gid://gitlab/DastSiteProfile/1"
> >
<div <div
class="gl-card-header" class="gl-card-header"
......
...@@ -2,7 +2,7 @@ import { GlDropdownItem } from '@gitlab/ui'; ...@@ -2,7 +2,7 @@ import { GlDropdownItem } from '@gitlab/ui';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { merge } from 'lodash'; import { merge } from 'lodash';
import OnDemandScansProfileSelector from 'ee/on_demand_scans/components/profile_selector/profile_selector.vue'; import OnDemandScansProfileSelector from 'ee/on_demand_scans/components/profile_selector/profile_selector.vue';
import { scannerProfiles } from '../../mock_data'; import { scannerProfiles } from '../../mocks/mock_data';
describe('OnDemandScansProfileSelector', () => { describe('OnDemandScansProfileSelector', () => {
let wrapper; let wrapper;
...@@ -41,12 +41,10 @@ describe('OnDemandScansProfileSelector', () => { ...@@ -41,12 +41,10 @@ describe('OnDemandScansProfileSelector', () => {
slots: { slots: {
title: 'Section title', title: 'Section title',
label: 'Use existing scanner profile', label: 'Use existing scanner profile',
summary: `<div>Profile's summary</div>`,
'no-profiles': 'No profile yet', 'no-profiles': 'No profile yet',
'new-profile': 'Create a new profile', 'new-profile': 'Create a new profile',
}, },
scopedSlots: {
summary: "<div>{{ props.profile.profileName }}'s summary</div>",
},
}, },
options, options,
), ),
...@@ -105,7 +103,7 @@ describe('OnDemandScansProfileSelector', () => { ...@@ -105,7 +103,7 @@ describe('OnDemandScansProfileSelector', () => {
it('when a profile is selected, input event is emitted', async () => { it('when a profile is selected, input event is emitted', async () => {
await selectFirstProfile(); await selectFirstProfile();
expect(wrapper.emitted('input')).toEqual([[scannerProfiles[0]]]); expect(wrapper.emitted('input')).toEqual([[scannerProfiles[0].id]]);
}); });
it('shows dropdown items for each profile', () => { it('shows dropdown items for each profile', () => {
...@@ -130,7 +128,7 @@ describe('OnDemandScansProfileSelector', () => { ...@@ -130,7 +128,7 @@ describe('OnDemandScansProfileSelector', () => {
createFullComponent({ createFullComponent({
propsData: { propsData: {
profiles: scannerProfiles, profiles: scannerProfiles,
value: selectedProfile, value: selectedProfile.id,
}, },
}); });
}); });
...@@ -139,7 +137,7 @@ describe('OnDemandScansProfileSelector', () => { ...@@ -139,7 +137,7 @@ describe('OnDemandScansProfileSelector', () => {
const summary = findSelectedProfileSummary(); const summary = findSelectedProfileSummary();
expect(summary.exists()).toBe(true); expect(summary.exists()).toBe(true);
expect(summary.text()).toContain(`${scannerProfiles[0].profileName}'s summary`); expect(summary.text()).toContain(`Profile's summary`);
}); });
it('displays item as checked', () => { it('displays item as checked', () => {
......
...@@ -2,7 +2,7 @@ import { mount, shallowMount } from '@vue/test-utils'; ...@@ -2,7 +2,7 @@ import { mount, shallowMount } from '@vue/test-utils';
import { merge } from 'lodash'; import { merge } from 'lodash';
import ProfileSelector from 'ee/on_demand_scans/components/profile_selector/profile_selector.vue'; import ProfileSelector from 'ee/on_demand_scans/components/profile_selector/profile_selector.vue';
import OnDemandScansScannerProfileSelector from 'ee/on_demand_scans/components/profile_selector/scanner_profile_selector.vue'; import OnDemandScansScannerProfileSelector from 'ee/on_demand_scans/components/profile_selector/scanner_profile_selector.vue';
import { scannerProfiles } from '../../mock_data'; import { scannerProfiles } from '../../mocks/mock_data';
const TEST_LIBRARY_PATH = '/test/scanner/profiles/library/path'; const TEST_LIBRARY_PATH = '/test/scanner/profiles/library/path';
const TEST_NEW_PATH = '/test/new/scanner/profile/path'; const TEST_NEW_PATH = '/test/new/scanner/profile/path';
...@@ -31,8 +31,8 @@ describe('OnDemandScansScannerProfileSelector', () => { ...@@ -31,8 +31,8 @@ describe('OnDemandScansScannerProfileSelector', () => {
newScannerProfilePath: TEST_NEW_PATH, newScannerProfilePath: TEST_NEW_PATH,
glFeatures: { securityOnDemandScansSiteValidation: true }, glFeatures: { securityOnDemandScansSiteValidation: true },
}, },
scopedSlots: { slots: {
summary: '<div slot-scope="{ profile }">{{ profile.profileName }}\'s summary</div>', summary: `<div>${profiles[0].profileName}'s summary</div>`,
}, },
}, },
options, options,
...@@ -50,7 +50,7 @@ describe('OnDemandScansScannerProfileSelector', () => { ...@@ -50,7 +50,7 @@ describe('OnDemandScansScannerProfileSelector', () => {
it('renders properly with profiles', () => { it('renders properly with profiles', () => {
createFullComponent({ createFullComponent({
propsData: { profiles, value: profiles[0] }, propsData: { profiles, value: profiles[0].id },
}); });
expect(wrapper.element).toMatchSnapshot(); expect(wrapper.element).toMatchSnapshot();
......
...@@ -2,7 +2,7 @@ import { mount, shallowMount } from '@vue/test-utils'; ...@@ -2,7 +2,7 @@ import { mount, shallowMount } from '@vue/test-utils';
import { merge } from 'lodash'; import { merge } from 'lodash';
import ProfileSelector from 'ee/on_demand_scans/components/profile_selector/profile_selector.vue'; import ProfileSelector from 'ee/on_demand_scans/components/profile_selector/profile_selector.vue';
import OnDemandScansSiteProfileSelector from 'ee/on_demand_scans/components/profile_selector/site_profile_selector.vue'; import OnDemandScansSiteProfileSelector from 'ee/on_demand_scans/components/profile_selector/site_profile_selector.vue';
import { siteProfiles } from '../../mock_data'; import { siteProfiles } from '../../mocks/mock_data';
const TEST_LIBRARY_PATH = '/test/site/profiles/library/path'; const TEST_LIBRARY_PATH = '/test/site/profiles/library/path';
const TEST_NEW_PATH = '/test/new/site/profile/path'; const TEST_NEW_PATH = '/test/new/site/profile/path';
...@@ -34,8 +34,8 @@ describe('OnDemandScansSiteProfileSelector', () => { ...@@ -34,8 +34,8 @@ describe('OnDemandScansSiteProfileSelector', () => {
newSiteProfilePath: TEST_NEW_PATH, newSiteProfilePath: TEST_NEW_PATH,
glFeatures: { securityOnDemandScansSiteValidation: true }, glFeatures: { securityOnDemandScansSiteValidation: true },
}, },
scopedSlots: { slots: {
summary: '<div slot-scope="{ profile }">{{ profile.profileName }}\'s summary</div>', summary: `<div>${profiles[0].profileName}'s summary</div>`,
}, },
}, },
options, options,
...@@ -53,7 +53,7 @@ describe('OnDemandScansSiteProfileSelector', () => { ...@@ -53,7 +53,7 @@ describe('OnDemandScansSiteProfileSelector', () => {
it('renders properly with profiles', () => { it('renders properly with profiles', () => {
createFullComponent({ createFullComponent({
propsData: { profiles, value: profiles[0] }, propsData: { profiles, value: profiles[0].id },
}); });
expect(wrapper.element).toMatchSnapshot(); expect(wrapper.element).toMatchSnapshot();
......
import { scannerProfiles, siteProfiles } from './mock_data';
const defaults = {
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
endCursor: null,
},
};
export const dastScannerProfiles = (profiles = scannerProfiles) => ({
data: {
project: {
scannerProfiles: {
...defaults,
edges: profiles.map(profile => ({
cursor: '',
node: profile,
})),
},
},
},
});
export const dastSiteProfiles = (profiles = siteProfiles) => ({
data: {
project: {
siteProfiles: {
...defaults,
edges: profiles.map(profile => ({
cursor: '',
node: profile,
})),
},
},
},
});
...@@ -7,6 +7,7 @@ export const scannerProfiles = [ ...@@ -7,6 +7,7 @@ export const scannerProfiles = [
scanType: 'PASSIVE', scanType: 'PASSIVE',
useAjaxSpider: false, useAjaxSpider: false,
showDebugMessages: false, showDebugMessages: false,
editPath: '/scanner_profile/edit/1',
}, },
{ {
id: 'gid://gitlab/DastScannerProfile/2', id: 'gid://gitlab/DastScannerProfile/2',
...@@ -16,6 +17,7 @@ export const scannerProfiles = [ ...@@ -16,6 +17,7 @@ export const scannerProfiles = [
scanType: 'ACTIVE', scanType: 'ACTIVE',
useAjaxSpider: true, useAjaxSpider: true,
showDebugMessages: true, showDebugMessages: true,
editPath: '/scanner_profile/edit/2',
}, },
]; ];
...@@ -24,12 +26,14 @@ export const siteProfiles = [ ...@@ -24,12 +26,14 @@ export const siteProfiles = [
id: 'gid://gitlab/DastSiteProfile/1', id: 'gid://gitlab/DastSiteProfile/1',
profileName: 'Site profile #1', profileName: 'Site profile #1',
targetUrl: 'https://foo.com', targetUrl: 'https://foo.com',
editPath: '/site_profiles/edit/1',
validationStatus: 'PENDING_VALIDATION', validationStatus: 'PENDING_VALIDATION',
}, },
{ {
id: 'gid://gitlab/DastSiteProfile/2', id: 'gid://gitlab/DastSiteProfile/2',
profileName: 'Site profile #2', profileName: 'Site profile #2',
targetUrl: 'https://bar.com', targetUrl: 'https://bar.com',
editPath: '/site_profiles/edit/2',
validationStatus: 'PASSED_VALIDATION', validationStatus: 'PASSED_VALIDATION',
}, },
]; ];
...@@ -6,7 +6,7 @@ import DastScannerProfileForm from 'ee/security_configuration/dast_scanner_profi ...@@ -6,7 +6,7 @@ import DastScannerProfileForm from 'ee/security_configuration/dast_scanner_profi
import { SCAN_TYPE } from 'ee/security_configuration/dast_scanner_profiles/constants'; import { SCAN_TYPE } from 'ee/security_configuration/dast_scanner_profiles/constants';
import dastScannerProfileCreateMutation from 'ee/security_configuration/dast_scanner_profiles/graphql/dast_scanner_profile_create.mutation.graphql'; import dastScannerProfileCreateMutation from 'ee/security_configuration/dast_scanner_profiles/graphql/dast_scanner_profile_create.mutation.graphql';
import dastScannerProfileUpdateMutation from 'ee/security_configuration/dast_scanner_profiles/graphql/dast_scanner_profile_update.mutation.graphql'; import dastScannerProfileUpdateMutation from 'ee/security_configuration/dast_scanner_profiles/graphql/dast_scanner_profile_update.mutation.graphql';
import { scannerProfiles } from 'ee_jest/on_demand_scans/mock_data'; import { scannerProfiles } from 'ee_jest/on_demand_scans/mocks/mock_data';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import { redirectTo } from '~/lib/utils/url_utility'; import { redirectTo } from '~/lib/utils/url_utility';
......
...@@ -7,7 +7,7 @@ import VueApollo from 'vue-apollo'; ...@@ -7,7 +7,7 @@ import VueApollo from 'vue-apollo';
import DastSiteProfileForm from 'ee/security_configuration/dast_site_profiles_form/components/dast_site_profile_form.vue'; import DastSiteProfileForm from 'ee/security_configuration/dast_site_profiles_form/components/dast_site_profile_form.vue';
import dastSiteProfileCreateMutation from 'ee/security_configuration/dast_site_profiles_form/graphql/dast_site_profile_create.mutation.graphql'; import dastSiteProfileCreateMutation from 'ee/security_configuration/dast_site_profiles_form/graphql/dast_site_profile_create.mutation.graphql';
import dastSiteProfileUpdateMutation from 'ee/security_configuration/dast_site_profiles_form/graphql/dast_site_profile_update.mutation.graphql'; import dastSiteProfileUpdateMutation from 'ee/security_configuration/dast_site_profiles_form/graphql/dast_site_profile_update.mutation.graphql';
import { siteProfiles } from 'ee_jest/on_demand_scans/mock_data'; import { siteProfiles } from 'ee_jest/on_demand_scans/mocks/mock_data';
import * as responses from 'ee_jest/security_configuration/dast_site_profiles_form/mock_data/apollo_mock'; import * as responses from 'ee_jest/security_configuration/dast_site_profiles_form/mock_data/apollo_mock';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'jest/helpers/wait_for_promises'; import waitForPromises from 'jest/helpers/wait_for_promises';
......
...@@ -14,7 +14,7 @@ module Gitlab ...@@ -14,7 +14,7 @@ module Gitlab
private private
def can_push? def can_push?
user_access.can_do_action?(:push_code) || user_access.can_push_to_branch?(ref) ||
project.branch_allows_collaboration?(user_access.user, branch_name) project.branch_allows_collaboration?(user_access.user, branch_name)
end end
end end
......
...@@ -319,11 +319,11 @@ module Gitlab ...@@ -319,11 +319,11 @@ module Gitlab
end end
def check_change_access! def check_change_access!
# Deploy keys with write access can push anything return if deploy_key? && !deploy_keys_on_protected_branches_enabled?
return if deploy_key?
if changes == ANY if changes == ANY
can_push = user_can_push? || can_push = (deploy_key? && deploy_keys_on_protected_branches_enabled?) ||
user_can_push? ||
project&.any_branch_allows_collaboration?(user_access.user) project&.any_branch_allows_collaboration?(user_access.user)
unless can_push unless can_push
...@@ -449,6 +449,8 @@ module Gitlab ...@@ -449,6 +449,8 @@ module Gitlab
CiAccess.new CiAccess.new
elsif user && request_from_ci_build? elsif user && request_from_ci_build?
BuildAccess.new(user, container: container) BuildAccess.new(user, container: container)
elsif deploy_key? && deploy_keys_on_protected_branches_enabled?
DeployKeyAccess.new(deploy_key, container: container)
else else
UserAccess.new(user, container: container) UserAccess.new(user, container: container)
end end
...@@ -526,6 +528,10 @@ module Gitlab ...@@ -526,6 +528,10 @@ module Gitlab
def size_checker def size_checker
container.repository_size_checker container.repository_size_checker
end end
def deploy_keys_on_protected_branches_enabled?
Feature.enabled?(:deploy_keys_on_protected_branches, project)
end
end end
end end
......
...@@ -12,7 +12,7 @@ RSpec.describe Gitlab::Checks::PushCheck do ...@@ -12,7 +12,7 @@ RSpec.describe Gitlab::Checks::PushCheck do
context 'when the user is not allowed to push to the repo' do context 'when the user is not allowed to push to the repo' do
it 'raises an error' do it 'raises an error' do
expect(user_access).to receive(:can_do_action?).with(:push_code).and_return(false) expect(user_access).to receive(:can_push_to_branch?).and_return(false)
expect(project).to receive(:branch_allows_collaboration?).with(user_access.user, 'master').and_return(false) expect(project).to receive(:branch_allows_collaboration?).with(user_access.user, 'master').and_return(false)
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'You are not allowed to push code to this project.') expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'You are not allowed to push code to this project.')
......
...@@ -262,14 +262,14 @@ RSpec.describe Clusters::Cluster, :use_clean_rails_memory_store_caching do ...@@ -262,14 +262,14 @@ RSpec.describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
end end
end end
describe '.with_project_alert_service_data' do describe '.with_project_http_integrations' do
subject { described_class.with_project_alert_service_data(project_id) } subject { described_class.with_project_http_integrations(project_id) }
let!(:cluster) { create(:cluster, :project) } let!(:cluster) { create(:cluster, :project) }
let!(:project_id) { cluster.first_project.id } let!(:project_id) { cluster.first_project.id }
context 'project has alert service data' do context 'project has alert service data' do
let!(:alerts_service) { create(:alerts_service, project: cluster.clusterable) } let!(:integration) { create(:alert_management_http_integration, project: cluster.clusterable) }
it { is_expected.to include(cluster) } it { is_expected.to include(cluster) }
end end
......
...@@ -75,4 +75,18 @@ RSpec.describe ProtectedBranch::PushAccessLevel do ...@@ -75,4 +75,18 @@ RSpec.describe ProtectedBranch::PushAccessLevel do
end end
end end
end end
describe '#type' do
let(:push_level_access) { build(:protected_branch_push_access_level) }
it 'returns :deploy_key when a deploy key is tied to the protected branch' do
push_level_access.deploy_key = create(:deploy_key)
expect(push_level_access.type).to eq(:deploy_key)
end
it 'returns :role by default' do
expect(push_level_access.type).to eq(:role)
end
end
end end
...@@ -18,7 +18,7 @@ RSpec.describe Clusters::Applications::PrometheusHealthCheckService, '#execute' ...@@ -18,7 +18,7 @@ RSpec.describe Clusters::Applications::PrometheusHealthCheckService, '#execute'
RSpec.shared_examples 'sends alert' do RSpec.shared_examples 'sends alert' do
it 'sends an alert' do it 'sends an alert' do
expect_next_instance_of(Projects::Alerting::NotifyService) do |notify_service| expect_next_instance_of(Projects::Alerting::NotifyService) do |notify_service|
expect(notify_service).to receive(:execute).with(alerts_service.token) expect(notify_service).to receive(:execute).with(integration.token, integration)
end end
subject subject
...@@ -40,8 +40,8 @@ RSpec.describe Clusters::Applications::PrometheusHealthCheckService, '#execute' ...@@ -40,8 +40,8 @@ RSpec.describe Clusters::Applications::PrometheusHealthCheckService, '#execute'
end end
context 'when cluster is project_type' do context 'when cluster is project_type' do
let_it_be(:alerts_service) { create(:alerts_service) } let_it_be(:project) { create(:project) }
let_it_be(:project) { create(:project, alerts_service: alerts_service) } let_it_be(:integration) { create(:alert_management_http_integration, project: project) }
let(:applications_prometheus_healthy) { true } let(:applications_prometheus_healthy) { true }
let(:prometheus) { create(:clusters_applications_prometheus, status: prometheus_status_value, healthy: applications_prometheus_healthy) } let(:prometheus) { create(:clusters_applications_prometheus, status: prometheus_status_value, healthy: applications_prometheus_healthy) }
let(:cluster) { create(:cluster, :project, application_prometheus: prometheus, projects: [project]) } let(:cluster) { create(:cluster, :project, application_prometheus: prometheus, projects: [project]) }
......
...@@ -8,7 +8,7 @@ RSpec.describe Clusters::Applications::CheckPrometheusHealthWorker, '#perform' d ...@@ -8,7 +8,7 @@ RSpec.describe Clusters::Applications::CheckPrometheusHealthWorker, '#perform' d
it 'triggers health service' do it 'triggers health service' do
cluster = create(:cluster) cluster = create(:cluster)
allow(Gitlab::Monitor::DemoProjects).to receive(:primary_keys) allow(Gitlab::Monitor::DemoProjects).to receive(:primary_keys)
allow(Clusters::Cluster).to receive_message_chain(:with_application_prometheus, :with_project_alert_service_data).and_return([cluster]) allow(Clusters::Cluster).to receive_message_chain(:with_application_prometheus, :with_project_http_integrations).and_return([cluster])
service_instance = instance_double(Clusters::Applications::PrometheusHealthCheckService) service_instance = instance_double(Clusters::Applications::PrometheusHealthCheckService)
expect(Clusters::Applications::PrometheusHealthCheckService).to receive(:new).with(cluster).and_return(service_instance) expect(Clusters::Applications::PrometheusHealthCheckService).to receive(:new).with(cluster).and_return(service_instance)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment