Commit c073f63d authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 087b0674 7dc2df6d
...@@ -315,12 +315,18 @@ module ApplicationSettingsHelper ...@@ -315,12 +315,18 @@ module ApplicationSettingsHelper
:throttle_authenticated_packages_api_enabled, :throttle_authenticated_packages_api_enabled,
:throttle_authenticated_packages_api_period_in_seconds, :throttle_authenticated_packages_api_period_in_seconds,
:throttle_authenticated_packages_api_requests_per_period, :throttle_authenticated_packages_api_requests_per_period,
:throttle_authenticated_files_api_enabled,
:throttle_authenticated_files_api_period_in_seconds,
:throttle_authenticated_files_api_requests_per_period,
:throttle_unauthenticated_enabled, :throttle_unauthenticated_enabled,
:throttle_unauthenticated_period_in_seconds, :throttle_unauthenticated_period_in_seconds,
:throttle_unauthenticated_requests_per_period, :throttle_unauthenticated_requests_per_period,
:throttle_unauthenticated_packages_api_enabled, :throttle_unauthenticated_packages_api_enabled,
:throttle_unauthenticated_packages_api_period_in_seconds, :throttle_unauthenticated_packages_api_period_in_seconds,
:throttle_unauthenticated_packages_api_requests_per_period, :throttle_unauthenticated_packages_api_requests_per_period,
:throttle_unauthenticated_files_api_enabled,
:throttle_unauthenticated_files_api_period_in_seconds,
:throttle_unauthenticated_files_api_requests_per_period,
:throttle_protected_paths_enabled, :throttle_protected_paths_enabled,
:throttle_protected_paths_period_in_seconds, :throttle_protected_paths_period_in_seconds,
:throttle_protected_paths_requests_per_period, :throttle_protected_paths_requests_per_period,
......
...@@ -479,6 +479,14 @@ class ApplicationSetting < ApplicationRecord ...@@ -479,6 +479,14 @@ class ApplicationSetting < ApplicationRecord
presence: true, presence: true,
numericality: { only_integer: true, greater_than: 0 } numericality: { only_integer: true, greater_than: 0 }
validates :throttle_unauthenticated_files_api_requests_per_period,
presence: true,
numericality: { only_integer: true, greater_than: 0 }
validates :throttle_unauthenticated_files_api_period_in_seconds,
presence: true,
numericality: { only_integer: true, greater_than: 0 }
validates :throttle_authenticated_api_requests_per_period, validates :throttle_authenticated_api_requests_per_period,
presence: true, presence: true,
numericality: { only_integer: true, greater_than: 0 } numericality: { only_integer: true, greater_than: 0 }
...@@ -503,6 +511,14 @@ class ApplicationSetting < ApplicationRecord ...@@ -503,6 +511,14 @@ class ApplicationSetting < ApplicationRecord
presence: true, presence: true,
numericality: { only_integer: true, greater_than: 0 } numericality: { only_integer: true, greater_than: 0 }
validates :throttle_authenticated_files_api_requests_per_period,
presence: true,
numericality: { only_integer: true, greater_than: 0 }
validates :throttle_authenticated_files_api_period_in_seconds,
presence: true,
numericality: { only_integer: true, greater_than: 0 }
validates :throttle_protected_paths_requests_per_period, validates :throttle_protected_paths_requests_per_period,
presence: true, presence: true,
numericality: { only_integer: true, greater_than: 0 } numericality: { only_integer: true, greater_than: 0 }
......
...@@ -167,6 +167,9 @@ module ApplicationSettingImplementation ...@@ -167,6 +167,9 @@ module ApplicationSettingImplementation
throttle_authenticated_packages_api_enabled: false, throttle_authenticated_packages_api_enabled: false,
throttle_authenticated_packages_api_period_in_seconds: 15, throttle_authenticated_packages_api_period_in_seconds: 15,
throttle_authenticated_packages_api_requests_per_period: 1000, throttle_authenticated_packages_api_requests_per_period: 1000,
throttle_authenticated_files_api_enabled: false,
throttle_authenticated_files_api_period_in_seconds: 15,
throttle_authenticated_files_api_requests_per_period: 500,
throttle_incident_management_notification_enabled: false, throttle_incident_management_notification_enabled: false,
throttle_incident_management_notification_per_period: 3600, throttle_incident_management_notification_per_period: 3600,
throttle_incident_management_notification_period_in_seconds: 3600, throttle_incident_management_notification_period_in_seconds: 3600,
...@@ -179,6 +182,9 @@ module ApplicationSettingImplementation ...@@ -179,6 +182,9 @@ module ApplicationSettingImplementation
throttle_unauthenticated_packages_api_enabled: false, throttle_unauthenticated_packages_api_enabled: false,
throttle_unauthenticated_packages_api_period_in_seconds: 15, throttle_unauthenticated_packages_api_period_in_seconds: 15,
throttle_unauthenticated_packages_api_requests_per_period: 800, throttle_unauthenticated_packages_api_requests_per_period: 800,
throttle_unauthenticated_files_api_enabled: false,
throttle_unauthenticated_files_api_period_in_seconds: 15,
throttle_unauthenticated_files_api_requests_per_period: 125,
time_tracking_limit_to_hours: false, time_tracking_limit_to_hours: false,
two_factor_grace_period: 48, two_factor_grace_period: 48,
unique_ips_limit_enabled: false, unique_ips_limit_enabled: false,
......
# frozen_string_literal: true
class AddThrottleFilesApiColumns < ActiveRecord::Migration[6.1]
def change
add_column :application_settings, :throttle_unauthenticated_files_api_requests_per_period, :integer, default: 125, null: false
add_column :application_settings, :throttle_unauthenticated_files_api_period_in_seconds, :integer, default: 15, null: false
add_column :application_settings, :throttle_authenticated_files_api_requests_per_period, :integer, default: 500, null: false
add_column :application_settings, :throttle_authenticated_files_api_period_in_seconds, :integer, default: 15, null: false
add_column :application_settings, :throttle_unauthenticated_files_api_enabled, :boolean, default: false, null: false
add_column :application_settings, :throttle_authenticated_files_api_enabled, :boolean, default: false, null: false
end
end
5c74d34171ed9129ffbb3efe5417da1ba857cd729837544e58074debd5afca88
\ No newline at end of file
...@@ -9606,6 +9606,12 @@ CREATE TABLE application_settings ( ...@@ -9606,6 +9606,12 @@ CREATE TABLE application_settings (
encrypted_customers_dot_jwt_signing_key bytea, encrypted_customers_dot_jwt_signing_key bytea,
encrypted_customers_dot_jwt_signing_key_iv bytea, encrypted_customers_dot_jwt_signing_key_iv bytea,
pypi_package_requests_forwarding boolean DEFAULT true NOT NULL, pypi_package_requests_forwarding boolean DEFAULT true NOT NULL,
throttle_unauthenticated_files_api_requests_per_period integer DEFAULT 125 NOT NULL,
throttle_unauthenticated_files_api_period_in_seconds integer DEFAULT 15 NOT NULL,
throttle_authenticated_files_api_requests_per_period integer DEFAULT 500 NOT NULL,
throttle_authenticated_files_api_period_in_seconds integer DEFAULT 15 NOT NULL,
throttle_unauthenticated_files_api_enabled boolean DEFAULT false NOT NULL,
throttle_authenticated_files_api_enabled boolean DEFAULT false NOT NULL,
CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)), CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)),
CONSTRAINT app_settings_ext_pipeline_validation_service_url_text_limit CHECK ((char_length(external_pipeline_validation_service_url) <= 255)), CONSTRAINT app_settings_ext_pipeline_validation_service_url_text_limit CHECK ((char_length(external_pipeline_validation_service_url) <= 255)),
CONSTRAINT app_settings_registry_exp_policies_worker_capacity_positive CHECK ((container_registry_expiration_policies_worker_capacity >= 0)), CONSTRAINT app_settings_registry_exp_policies_worker_capacity_positive CHECK ((container_registry_expiration_policies_worker_capacity >= 0)),
...@@ -15,7 +15,7 @@ Available resources for the [GitLab REST API](index.md) can be grouped in the fo ...@@ -15,7 +15,7 @@ Available resources for the [GitLab REST API](index.md) can be grouped in the fo
See also: See also:
- [V3 to V4](v3_to_v4.md). - [V3 to V4](v3_to_v4.md).
- Adding [deploy keys for multiple projects](deploy_keys.md#adding-deploy-keys-to-multiple-projects). - Adding [deploy keys for multiple projects](deploy_keys.md#add-deploy-keys-to-multiple-projects).
- [API Resources for various templates](#templates-api-resources). - [API Resources for various templates](#templates-api-resources).
## Project resources ## Project resources
......
...@@ -8,7 +8,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w ...@@ -8,7 +8,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w
## List all deploy keys ## List all deploy keys
Get a list of all deploy keys across all projects of the GitLab instance. This endpoint requires administrator access and is not available on GitLab.com. Get a list of all deploy keys across all projects of the GitLab instance. This
endpoint requires an administrator role and is not available on GitLab.com.
```plaintext ```plaintext
GET /deploy_keys GET /deploy_keys
...@@ -74,7 +75,7 @@ Example response: ...@@ -74,7 +75,7 @@ Example response:
] ]
``` ```
## Single deploy key ## Get a single deploy key
Get a single key. Get a single key.
...@@ -213,10 +214,10 @@ Example response: ...@@ -213,10 +214,10 @@ Example response:
} }
``` ```
## Adding deploy keys to multiple projects ## Add deploy keys to multiple projects
If you want to easily add the same deploy key to multiple projects in the same If you want to add the same deploy key to multiple projects in the same
group, this can be achieved quite easily with the API. group, this can be achieved with the API.
First, find the ID of the projects you're interested in, by either listing all First, find the ID of the projects you're interested in, by either listing all
projects: projects:
......
...@@ -5,7 +5,7 @@ group: Access ...@@ -5,7 +5,7 @@ group: Access
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
--- ---
# GitLab as an OAuth2 provider # GitLab as an OAuth 2.0 provider
This document covers using the [OAuth2](https://oauth.net/2/) protocol to allow This document covers using the [OAuth2](https://oauth.net/2/) protocol to allow
other services to access GitLab resources on user's behalf. other services to access GitLab resources on user's behalf.
...@@ -15,9 +15,9 @@ other services, see the [OAuth2 authentication service provider](../integration/ ...@@ -15,9 +15,9 @@ other services, see the [OAuth2 authentication service provider](../integration/
documentation. This functionality is based on the documentation. This functionality is based on the
[doorkeeper Ruby gem](https://github.com/doorkeeper-gem/doorkeeper). [doorkeeper Ruby gem](https://github.com/doorkeeper-gem/doorkeeper).
## Supported OAuth2 flows ## Supported OAuth 2.0 flows
GitLab currently supports the following authorization flows: GitLab supports the following authorization flows:
- **Authorization code with [Proof Key for Code Exchange (PKCE)](https://tools.ietf.org/html/rfc7636):** - **Authorization code with [Proof Key for Code Exchange (PKCE)](https://tools.ietf.org/html/rfc7636):**
Most secure. Without PKCE, you'd have to include client secrets on mobile clients, Most secure. Without PKCE, you'd have to include client secrets on mobile clients,
...@@ -26,14 +26,13 @@ GitLab currently supports the following authorization flows: ...@@ -26,14 +26,13 @@ GitLab currently supports the following authorization flows:
server-side apps. server-side apps.
- **Implicit grant:** Originally designed for user-agent only apps, such as - **Implicit grant:** Originally designed for user-agent only apps, such as
single page web apps running on GitLab Pages). single page web apps running on GitLab Pages).
The [IETF](https://tools.ietf.org/html/draft-ietf-oauth-security-topics-09#section-2.1.2) The [Internet Engineering Task Force (IETF)](https://tools.ietf.org/html/draft-ietf-oauth-security-topics-09#section-2.1.2)
recommends against Implicit grant flow. recommends against Implicit grant flow.
- **Resource owner password credentials:** To be used **only** for securely - **Resource owner password credentials:** To be used **only** for securely
hosted, first-party services. GitLab recommends against use of this flow. hosted, first-party services. GitLab recommends against use of this flow.
The draft specification for [OAuth 2.1](https://oauth.net/2.1/) specifically omits both the The draft specification for [OAuth 2.1](https://oauth.net/2.1/) specifically omits both the
Implicit grant and Resource Owner Password Credentials flows. Implicit grant and Resource Owner Password Credentials flows. It will be deprecated in the next OAuth specification version.
it will be deprecated in the next OAuth specification version.
Refer to the [OAuth RFC](https://tools.ietf.org/html/rfc6749) to find out Refer to the [OAuth RFC](https://tools.ietf.org/html/rfc6749) to find out
how all those flows work and pick the right one for your use case. how all those flows work and pick the right one for your use case.
...@@ -57,7 +56,7 @@ parameter, which are securely bound to the user agent", with each request to the ...@@ -57,7 +56,7 @@ parameter, which are securely bound to the user agent", with each request to the
For production, please use HTTPS for your `redirect_uri`. For production, please use HTTPS for your `redirect_uri`.
For development, GitLab allows insecure HTTP redirect URIs. For development, GitLab allows insecure HTTP redirect URIs.
As OAuth2 bases its security entirely on the transport layer, you should not use unprotected As OAuth 2.0 bases its security entirely on the transport layer, you should not use unprotected
URIs. For more information, see the [OAuth 2.0 RFC](https://tools.ietf.org/html/rfc6749#section-3.1.2.1) URIs. For more information, see the [OAuth 2.0 RFC](https://tools.ietf.org/html/rfc6749#section-3.1.2.1)
and the [OAuth 2.0 Threat Model RFC](https://tools.ietf.org/html/rfc6819#section-4.4.2.1). and the [OAuth 2.0 Threat Model RFC](https://tools.ietf.org/html/rfc6819#section-4.4.2.1).
These factors are particularly important when using the These factors are particularly important when using the
...@@ -123,7 +122,7 @@ Before starting the flow, generate the `STATE`, the `CODE_VERIFIER` and the `COD ...@@ -123,7 +122,7 @@ Before starting the flow, generate the `STATE`, the `CODE_VERIFIER` and the `COD
"created_at": 1607635748 "created_at": 1607635748
} }
``` ```
1. To retrieve a new `access_token`, use the `refresh_token` parameter. Refresh tokens may 1. To retrieve a new `access_token`, use the `refresh_token` parameter. Refresh tokens may
be used even after the `access_token` itself expires. This request: be used even after the `access_token` itself expires. This request:
- Invalidates the existing `access_token` and `refresh_token`. - Invalidates the existing `access_token` and `refresh_token`.
...@@ -135,7 +134,7 @@ Before starting the flow, generate the `STATE`, the `CODE_VERIFIER` and the `COD ...@@ -135,7 +134,7 @@ Before starting the flow, generate the `STATE`, the `CODE_VERIFIER` and the `COD
``` ```
Example response: Example response:
```json ```json
{ {
"access_token": "c97d1fe52119f38c7f67f0a14db68d60caa35ddc86fd12401718b649dcfa9c68", "access_token": "c97d1fe52119f38c7f67f0a14db68d60caa35ddc86fd12401718b649dcfa9c68",
...@@ -203,7 +202,7 @@ be used as a CSRF token. ...@@ -203,7 +202,7 @@ be used as a CSRF token.
"created_at": 1607635748 "created_at": 1607635748
} }
``` ```
1. To retrieve a new `access_token`, use the `refresh_token` parameter. Refresh tokens may 1. To retrieve a new `access_token`, use the `refresh_token` parameter. Refresh tokens may
be used even after the `access_token` itself expires. This request: be used even after the `access_token` itself expires. This request:
- Invalidates the existing `access_token` and `refresh_token`. - Invalidates the existing `access_token` and `refresh_token`.
...@@ -245,12 +244,13 @@ scheduled to be removed for existing applications. ...@@ -245,12 +244,13 @@ scheduled to be removed for existing applications.
We recommend that you use [Authorization code with PKCE](#authorization-code-with-proof-key-for-code-exchange-pkce) instead. If you choose to use Implicit flow, be sure to verify the We recommend that you use [Authorization code with PKCE](#authorization-code-with-proof-key-for-code-exchange-pkce) instead. If you choose to use Implicit flow, be sure to verify the
`application id` (or `client_id`) associated with the access token before granting `application id` (or `client_id`) associated with the access token before granting
access to the data, as described in [Retrieving the token information](#retrieving-the-token-information)). access to the data. To learn more, read
[Retrieving the token information](#retrieve-the-token-information)).
Unlike the authorization code flow, the client receives an `access token` Unlike the authorization code flow, the client receives an `access token`
immediately as a result of the authorization request. The flow does not use immediately as a result of the authorization request. The flow does not use the
the client secret or the authorization code because all of the application code client secret or the authorization code, as the application
and storage is easily accessible on client browsers and mobile devices. code and storage is accessible on client browsers and mobile devices.
To request the access token, you should redirect the user to the To request the access token, you should redirect the user to the
`/oauth/authorize` endpoint using `token` response type: `/oauth/authorize` endpoint using `token` response type:
...@@ -367,10 +367,11 @@ or you can put the token to the Authorization header: ...@@ -367,10 +367,11 @@ or you can put the token to the Authorization header:
curl --header "Authorization: Bearer OAUTH-TOKEN" "https://gitlab.example.com/api/v4/user" curl --header "Authorization: Bearer OAUTH-TOKEN" "https://gitlab.example.com/api/v4/user"
``` ```
## Retrieving the token information ## Retrieve the token information
To verify the details of a token, use the `token/info` endpoint provided by the Doorkeeper gem. To verify the details of a token, use the `token/info` endpoint provided by the
For more information, see [`/oauth/token/info`](https://github.com/doorkeeper-gem/doorkeeper/wiki/API-endpoint-descriptions-and-examples#get----oauthtokeninfo). Doorkeeper gem. For more information, see
[`/oauth/token/info`](https://github.com/doorkeeper-gem/doorkeeper/wiki/API-endpoint-descriptions-and-examples#get----oauthtokeninfo).
You must supply the access token, either: You must supply the access token, either:
...@@ -407,9 +408,10 @@ prevent breaking changes introduced in [doorkeeper 5.0.2](https://github.com/doo ...@@ -407,9 +408,10 @@ prevent breaking changes introduced in [doorkeeper 5.0.2](https://github.com/doo
Don't rely on these fields as they are slated for removal in a later release. Don't rely on these fields as they are slated for removal in a later release.
## OAuth2 tokens and GitLab registries ## OAuth 2.0 tokens and GitLab registries
Standard OAuth2 tokens support different degrees of access to GitLab registries, as they: Standard OAuth 2.0 tokens support different degrees of access to GitLab
registries, as they:
- Do not allow users to authenticate to: - Do not allow users to authenticate to:
- The GitLab [Container registry](../user/packages/container_registry/index.md#authenticate-with-the-container-registry). - The GitLab [Container registry](../user/packages/container_registry/index.md#authenticate-with-the-container-registry).
......
...@@ -108,13 +108,14 @@ V1. ...@@ -108,13 +108,14 @@ V1.
GET group/:id/-/packages/composer/:package_name$:sha GET group/:id/-/packages/composer/:package_name$:sha
``` ```
Note the `$` symbol in the URL. When making requests, you may need to use the URL-encoded version of Note the `$` symbol in the URL. When making requests, you may need the
the symbol `%24` (see example below). URL-encoded version of the symbol `%24`. Refer to the example after
the table:
| Attribute | Type | Required | Description |
| -------------- | ------ | -------- | ----------- | | Attribute | Type | Required | Description |
| `id` | string | yes | The ID or full path of the group. | |----------------|--------|----------|---------------------------------------------------------------------------------------|
| `package_name` | string | yes | The name of the package. | | `id` | string | yes | The ID or full path of the group. |
| `package_name` | string | yes | The name of the package. |
| `sha` | string | yes | The SHA digest of the package, provided by the [V1 packages list](#v1-packages-list). | | `sha` | string | yes | The SHA digest of the package, provided by the [V1 packages list](#v1-packages-list). |
```shell ```shell
......
...@@ -58,11 +58,11 @@ Upload a package. ...@@ -58,11 +58,11 @@ Upload a package.
PUT projects/:id/packages/npm/:package_name PUT projects/:id/packages/npm/:package_name
``` ```
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
| ----------------- | ------ | -------- | ----------- | |----------------|--------|----------|-------------------------------------|
| `id` | string | yes | The ID or full path of the project. | | `id` | string | yes | The ID or full path of the project. |
| `package_name` | string | yes | The name of the package. | | `package_name` | string | yes | The name of the package. |
| `versions` | string | yes | Package version info. | | `versions` | string | yes | Package version information. |
```shell ```shell
curl --request PUT curl --request PUT
......
...@@ -47,7 +47,8 @@ To write the output to a file: ...@@ -47,7 +47,8 @@ To write the output to a file:
curl --user <username>:<personal_access_token> "https://gitlab.example.com/api/v4/groups/1/packages/pypi/files/5y57017232013c8ac80647f4ca153k3726f6cba62d055cd747844ed95b3c65ff/my.pypi.package-0.0.1.tar.gz" >> my.pypi.package-0.0.1.tar.gz curl --user <username>:<personal_access_token> "https://gitlab.example.com/api/v4/groups/1/packages/pypi/files/5y57017232013c8ac80647f4ca153k3726f6cba62d055cd747844ed95b3c65ff/my.pypi.package-0.0.1.tar.gz" >> my.pypi.package-0.0.1.tar.gz
``` ```
This writes the downloaded file to `my.pypi.package-0.0.1.tar.gz` in the current directory. This writes the downloaded file to `my.pypi.package-0.0.1.tar.gz` in the current
directory.
## Group level simple API entry point ## Group level simple API entry point
...@@ -106,7 +107,7 @@ GET projects/:id/packages/pypi/files/:sha256/:file_identifier ...@@ -106,7 +107,7 @@ GET projects/:id/packages/pypi/files/:sha256/:file_identifier
| --------- | ---- | -------- | ----------- | | --------- | ---- | -------- | ----------- |
| `id` | string | yes | The ID or full path of the project. | | `id` | string | yes | The ID or full path of the project. |
| `sha256` | string | yes | PyPI package file sha256 check sum. | | `sha256` | string | yes | PyPI package file sha256 check sum. |
| `file_identifier` | string | yes | The PyPI package file name. | | `file_identifier` | string | yes | The PyPI package filename. |
```shell ```shell
curl --user <username>:<personal_access_token> "https://gitlab.example.com/api/v4/projects/1/packages/pypi/files/5y57017232013c8ac80647f4ca153k3726f6cba62d055cd747844ed95b3c65ff/my.pypi.package-0.0.1.tar.gz" curl --user <username>:<personal_access_token> "https://gitlab.example.com/api/v4/projects/1/packages/pypi/files/5y57017232013c8ac80647f4ca153k3726f6cba62d055cd747844ed95b3c65ff/my.pypi.package-0.0.1.tar.gz"
...@@ -118,7 +119,8 @@ To write the output to a file: ...@@ -118,7 +119,8 @@ To write the output to a file:
curl --user <username>:<personal_access_token> "https://gitlab.example.com/api/v4/projects/1/packages/pypi/files/5y57017232013c8ac80647f4ca153k3726f6cba62d055cd747844ed95b3c65ff/my.pypi.package-0.0.1.tar.gz" >> my.pypi.package-0.0.1.tar.gz curl --user <username>:<personal_access_token> "https://gitlab.example.com/api/v4/projects/1/packages/pypi/files/5y57017232013c8ac80647f4ca153k3726f6cba62d055cd747844ed95b3c65ff/my.pypi.package-0.0.1.tar.gz" >> my.pypi.package-0.0.1.tar.gz
``` ```
This writes the downloaded file to `my.pypi.package-0.0.1.tar.gz` in the current directory. This writes the downloaded file to `my.pypi.package-0.0.1.tar.gz` in the current
directory.
## Project-level simple API entry point ## Project-level simple API entry point
......
...@@ -43,7 +43,7 @@ This page gathers all the resources for the topic **Authentication** within GitL ...@@ -43,7 +43,7 @@ This page gathers all the resources for the topic **Authentication** within GitL
- [Personal access tokens](../../api/index.md#personalproject-access-tokens) - [Personal access tokens](../../api/index.md#personalproject-access-tokens)
- [Project access tokens](../../api/index.md#personalproject-access-tokens) - [Project access tokens](../../api/index.md#personalproject-access-tokens)
- [Impersonation tokens](../../api/index.md#impersonation-tokens) - [Impersonation tokens](../../api/index.md#impersonation-tokens)
- [GitLab as an OAuth2 provider](../../api/oauth2.md#gitlab-as-an-oauth2-provider) - [GitLab as an OAuth2 provider](../../api/oauth2.md#gitlab-as-an-oauth-20-provider)
## Third-party resources ## Third-party resources
......
<script> <script>
import { GlFormGroup, GlFormInput, GlFormTextarea, GlToggle, GlButton, GlAlert } from '@gitlab/ui'; import {
GlEmptyState,
GlFormGroup,
GlFormInput,
GlFormTextarea,
GlLoadingIcon,
GlToggle,
GlButton,
GlAlert,
} from '@gitlab/ui';
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
import { removeUnnecessaryDashes } from 'ee/threat_monitoring/utils'; import { removeUnnecessaryDashes } from 'ee/threat_monitoring/utils';
import { redirectTo } from '~/lib/utils/url_utility'; import { redirectTo } from '~/lib/utils/url_utility';
import { s__ } from '~/locale'; import { __, s__ } from '~/locale';
import { EDITOR_MODES, EDITOR_MODE_YAML, PARSING_ERROR_MESSAGE } from '../constants'; import { EDITOR_MODES, EDITOR_MODE_YAML, PARSING_ERROR_MESSAGE } from '../constants';
import DimDisableContainer from '../dim_disable_container.vue'; import DimDisableContainer from '../dim_disable_container.vue';
import PolicyActionPicker from '../policy_action_picker.vue'; import PolicyActionPicker from '../policy_action_picker.vue';
...@@ -24,13 +33,19 @@ import PolicyRuleBuilder from './policy_rule_builder.vue'; ...@@ -24,13 +33,19 @@ import PolicyRuleBuilder from './policy_rule_builder.vue';
export default { export default {
EDITOR_MODES, EDITOR_MODES,
i18n: { i18n: {
toggleLabel: s__('NetworkPolicies|Policy status'), toggleLabel: s__('SecurityOrchestration|Policy status'),
PARSING_ERROR_MESSAGE, PARSING_ERROR_MESSAGE,
noEnvironmentDescription: s__(
'SecurityOrchestration|Network Policies can be used to limit which network traffic is allowed between containers inside the cluster.',
),
noEnvironmentButton: __('Learn more'),
}, },
components: { components: {
GlEmptyState,
GlFormGroup, GlFormGroup,
GlFormInput, GlFormInput,
GlFormTextarea, GlFormTextarea,
GlLoadingIcon,
GlToggle, GlToggle,
GlButton, GlButton,
GlAlert, GlAlert,
...@@ -41,7 +56,7 @@ export default { ...@@ -41,7 +56,7 @@ export default {
PolicyEditorLayout, PolicyEditorLayout,
DimDisableContainer, DimDisableContainer,
}, },
inject: ['threatMonitoringPath', 'projectId'], inject: ['networkDocumentationPath', 'noEnvironmentSvgPath', 'projectId', 'threatMonitoringPath'],
props: { props: {
existingPolicy: { existingPolicy: {
type: Object, type: Object,
...@@ -71,6 +86,9 @@ export default { ...@@ -71,6 +86,9 @@ export default {
}; };
}, },
computed: { computed: {
hasEnvironment() {
return Boolean(this.environments.length);
},
humanizedPolicy() { humanizedPolicy() {
return this.policy.error ? null : humanizeNetworkPolicy(this.policy); return this.policy.error ? null : humanizeNetworkPolicy(this.policy);
}, },
...@@ -80,7 +98,11 @@ export default { ...@@ -80,7 +98,11 @@ export default {
policyYaml() { policyYaml() {
return this.hasParsingError ? '' : toYaml(this.policy); return this.hasParsingError ? '' : toYaml(this.policy);
}, },
...mapState('threatMonitoring', ['currentEnvironmentId']), ...mapState('threatMonitoring', [
'currentEnvironmentId',
'environments',
'isLoadingEnvironments',
]),
...mapState('networkPolicies', [ ...mapState('networkPolicies', [
'isUpdatingPolicy', 'isUpdatingPolicy',
'isRemovingPolicy', 'isRemovingPolicy',
...@@ -159,7 +181,9 @@ export default { ...@@ -159,7 +181,9 @@ export default {
</script> </script>
<template> <template>
<gl-loading-icon v-if="isLoadingEnvironments" size="lg" />
<policy-editor-layout <policy-editor-layout
v-else-if="hasEnvironment"
:is-editing="isEditing" :is-editing="isEditing"
:is-removing-policy="isRemovingPolicy" :is-removing-policy="isRemovingPolicy"
:is-updating-policy="isUpdatingPolicy" :is-updating-policy="isUpdatingPolicy"
...@@ -255,4 +279,12 @@ export default { ...@@ -255,4 +279,12 @@ export default {
</dim-disable-container> </dim-disable-container>
</template> </template>
</policy-editor-layout> </policy-editor-layout>
<gl-empty-state
v-else
:description="$options.i18n.noEnvironmentDescription"
:primary-button-link="networkDocumentationPath"
:primary-button-text="$options.i18n.noEnvironmentButton"
:svg-path="noEnvironmentSvgPath"
title=""
/>
</template> </template>
...@@ -79,10 +79,10 @@ export default { ...@@ -79,10 +79,10 @@ export default {
{{ error }} {{ error }}
</gl-alert> </gl-alert>
<header class="gl-pb-5"> <header class="gl-pb-5">
<h3>{{ s__('NetworkPolicies|Policy description') }}</h3> <h3>{{ s__('SecurityOrchestration|Policy description') }}</h3>
</header> </header>
<div class="gl-display-flex"> <div class="gl-display-flex">
<gl-form-group :label="s__('NetworkPolicies|Policy type')" label-for="policyType"> <gl-form-group :label="s__('SecurityOrchestration|Policy type')" label-for="policyType">
<gl-form-select <gl-form-select
id="policyType" id="policyType"
:value="policyOptions.value" :value="policyOptions.value"
......
...@@ -20,7 +20,9 @@ export default () => { ...@@ -20,7 +20,9 @@ export default () => {
environmentsEndpoint, environmentsEndpoint,
configureAgentHelpPath, configureAgentHelpPath,
createAgentHelpPath, createAgentHelpPath,
networkDocumentationPath,
networkPoliciesEndpoint, networkPoliciesEndpoint,
noEnvironmentSvgPath,
threatMonitoringPath, threatMonitoringPath,
policy, policy,
policyType, policyType,
...@@ -58,6 +60,8 @@ export default () => { ...@@ -58,6 +60,8 @@ export default () => {
createAgentHelpPath, createAgentHelpPath,
disableScanExecutionUpdate: parseBoolean(disableScanExecutionUpdate), disableScanExecutionUpdate: parseBoolean(disableScanExecutionUpdate),
policyType, policyType,
networkDocumentationPath,
noEnvironmentSvgPath,
projectId, projectId,
projectPath, projectPath,
threatMonitoringPath, threatMonitoringPath,
......
...@@ -20,7 +20,9 @@ class IterationsFinder ...@@ -20,7 +20,9 @@ class IterationsFinder
@current_user = current_user @current_user = current_user
end end
def execute def execute(skip_authorization: false)
@skip_authorization = skip_authorization
items = Iteration.all items = Iteration.all
items = by_id(items) items = by_id(items)
items = by_iid(items) items = by_iid(items)
...@@ -36,8 +38,10 @@ class IterationsFinder ...@@ -36,8 +38,10 @@ class IterationsFinder
private private
attr_reader :skip_authorization
def by_groups(items) def by_groups(items)
return Iteration.none unless Ability.allowed?(current_user, :read_iteration, params[:parent]) return Iteration.none unless skip_authorization || Ability.allowed?(current_user, :read_iteration, params[:parent])
items.of_groups(groups) items.of_groups(groups)
end end
......
...@@ -28,6 +28,8 @@ module Projects::Security::PoliciesHelper ...@@ -28,6 +28,8 @@ module Projects::Security::PoliciesHelper
create_agent_help_path: help_page_url('user/clusters/agent/index.md', anchor: 'create-an-agent-record-in-gitlab'), create_agent_help_path: help_page_url('user/clusters/agent/index.md', anchor: 'create-an-agent-record-in-gitlab'),
environments_endpoint: project_environments_path(project), environments_endpoint: project_environments_path(project),
environment_id: environment&.id, environment_id: environment&.id,
network_documentation_path: help_page_path('user/application_security/threat_monitoring/index'),
no_environment_svg_path: image_path('illustrations/monitoring/unable_to_connect.svg'),
policy: policy&.to_json, policy: policy&.to_json,
policy_type: policy_type, policy_type: policy_type,
project_path: project.full_path, project_path: project.full_path,
......
...@@ -8,10 +8,56 @@ module EE ...@@ -8,10 +8,56 @@ module EE
module IterationReferenceFilter module IterationReferenceFilter
include ::Gitlab::Utils::StrongMemoize include ::Gitlab::Utils::StrongMemoize
def find_object(parent, id) def parent_records(parent, ids)
return unless valid_context?(parent) return Iteration.none unless valid_context?(parent)
find_iteration(parent, id: id) iteration_ids = ids.map {|y| y[:iteration_id]}.compact
unless iteration_ids.empty?
id_relation = find_iterations(parent, ids: iteration_ids)
end
iteration_names = ids.map {|y| y[:iteration_name]}.compact
unless iteration_names.empty?
iteration_relation = find_iterations(parent, names: iteration_names)
end
relation = [id_relation, iteration_relation].compact
return ::Iteration.none if relation.all?(::Iteration.none)
::Iteration.from_union(relation).includes(:project, :group) # rubocop: disable CodeReuse/ActiveRecord
end
def find_object(parent_object, id)
key = reference_cache.records_per_parent[parent_object].keys.find do |k|
k[:iteration_id] == id[:iteration_id] || k[:iteration_name] == id[:iteration_name]
end
reference_cache.records_per_parent[parent_object][key] if key
end
# Transform a symbol extracted from the text to a meaningful value
#
# This method has the contract that if a string `ref` refers to a
# record `record`, then `parse_symbol(ref) == record_identifier(record)`.
#
# This contract is slightly broken here, as we only have either the iteration_id
# or the iteration_name, but not both. But below, we have both pieces of information.
# It's accounted for in `find_object`
def parse_symbol(symbol, match_data)
if symbol
# when parsing links, there is no `match_data[:iteration_id]`, but `symbol`
# holds the id
{ iteration_id: symbol.to_i, iteration_name: nil }
else
{ iteration_id: match_data[:iteration_id]&.to_i, iteration_name: match_data[:iteration_name]&.tr('"', '') }
end
end
# This method has the contract that if a string `ref` refers to a
# record `record`, then `class.parse_symbol(ref) == record_identifier(record)`.
# See note in `parse_symbol` above
def record_identifier(record)
{ iteration_id: record.id, iteration_name: record.name }
end end
def valid_context?(parent) def valid_context?(parent)
...@@ -37,12 +83,14 @@ module EE ...@@ -37,12 +83,14 @@ module EE
return super(text, pattern) if pattern != ::Iteration.reference_pattern return super(text, pattern) if pattern != ::Iteration.reference_pattern
iterations = {} iterations = {}
unescaped_html = unescape_html_entities(text).gsub(pattern) do |match|
iteration = parse_and_find_iteration($~[:project], $~[:namespace], $~[:iteration_id], $~[:iteration_name])
if iteration unescaped_html = unescape_html_entities(text).gsub(pattern).with_index do |match, index|
iterations[iteration.id] = yield match, iteration.id, $~[:project], $~[:namespace], $~ ident = identifier($~)
"#{::Banzai::Filter::References::AbstractReferenceFilter::REFERENCE_PLACEHOLDER}#{iteration.id}" iteration = yield match, ident, $~[:project], $~[:namespace], $~
if iteration != match
iterations[index] = iteration
"#{::Banzai::Filter::References::AbstractReferenceFilter::REFERENCE_PLACEHOLDER}#{index}"
else else
match match
end end
...@@ -53,35 +101,16 @@ module EE ...@@ -53,35 +101,16 @@ module EE
escape_with_placeholders(unescaped_html, iterations) escape_with_placeholders(unescaped_html, iterations)
end end
def parse_and_find_iteration(project_ref, namespace_ref, iteration_id, iteration_name) def find_iterations(parent, ids: nil, names: nil)
project_path = reference_cache.full_project_path(namespace_ref, project_ref) finder_params = iteration_finder_params(parent, ids: ids, names: names)
# Returns group if project is not found by path
parent = parent_from_ref(project_path)
return unless parent
iteration_params = iteration_params(iteration_id, iteration_name)
find_iteration(parent, iteration_params)
end
def iteration_params(id, name) IterationsFinder.new(user, finder_params).execute(skip_authorization: true)
if name
{ name: name.tr('"', '') }
else
{ id: id.to_i }
end
end end
# rubocop: disable CodeReuse/ActiveRecord def iteration_finder_params(parent, ids: nil, names: nil)
def find_iteration(parent, params) parms = ids.present? ? { id: ids } : { title: names }
::Iteration.for_projects_and_groups(project_ids(parent), group_and_ancestors_ids(parent)).find_by(**params)
end
# rubocop: enable CodeReuse/ActiveRecord
def project_ids(parent) { parent: parent, include_ancestors: true }.merge(parms)
parent.id if project_context?(parent)
end end
def group_and_ancestors_ids(parent) def group_and_ancestors_ids(parent)
...@@ -94,8 +123,8 @@ module EE ...@@ -94,8 +123,8 @@ module EE
def url_for_object(iteration, _parent) def url_for_object(iteration, _parent)
::Gitlab::Routing ::Gitlab::Routing
.url_helpers .url_helpers
.iteration_url(iteration, only_path: context[:only_path]) .iteration_url(iteration, only_path: context[:only_path])
end end
def object_link_text(object, matches) def object_link_text(object, matches)
...@@ -112,6 +141,14 @@ module EE ...@@ -112,6 +141,14 @@ module EE
def object_link_title(_object, _matches) def object_link_title(_object, _matches)
'Iteration' 'Iteration'
end end
def parent
project || group
end
def requires_unescaping?
true
end
end end
end end
end end
......
...@@ -37,6 +37,14 @@ RSpec.describe IterationsFinder do ...@@ -37,6 +37,14 @@ RSpec.describe IterationsFinder do
expect(subject).to be_empty expect(subject).to be_empty
end end
end end
context 'when skipping authorization' do
let(:params) { { parent: parent } }
it 'returns iterations' do
expect(described_class.new(user, params).execute(skip_authorization: true)).not_to be_empty
end
end
end end
context 'with permissions' do context 'with permissions' do
......
import { GlToggle } from '@gitlab/ui'; import { GlEmptyState, GlLoadingIcon, GlToggle } from '@gitlab/ui';
import { EDITOR_MODE_YAML } from 'ee/threat_monitoring/components/policy_editor/constants'; import { EDITOR_MODE_YAML } from 'ee/threat_monitoring/components/policy_editor/constants';
import { import {
RuleDirectionInbound, RuleDirectionInbound,
...@@ -25,22 +25,33 @@ describe('NetworkPolicyEditor component', () => { ...@@ -25,22 +25,33 @@ describe('NetworkPolicyEditor component', () => {
let store; let store;
let wrapper; let wrapper;
const factory = ({ propsData, provide = {}, state, data } = {}) => { const defaultStore = { threatMonitoring: { environments: [{ id: 1 }], currentEnvironmentId: 1 } };
const factory = ({ propsData, provide = {}, updatedStore = defaultStore, data } = {}) => {
store = createStore(); store = createStore();
Object.assign(store.state.threatMonitoring, {
...state, store.replaceState({
}); ...store.state,
Object.assign(store.state.networkPolicies, { networkPolicies: {
...state, ...store.state.networkPolicies,
...updatedStore.networkPolicies,
},
threatMonitoring: {
...store.state.threatMonitoring,
...updatedStore.threatMonitoring,
},
}); });
jest.spyOn(store, 'dispatch').mockImplementation(() => Promise.resolve()); jest.spyOn(store, 'dispatch').mockImplementation(() => Promise.resolve());
wrapper = shallowMountExtended(NetworkPolicyEditor, { wrapper = shallowMountExtended(NetworkPolicyEditor, {
propsData: { propsData: {
hasEnvironment: true,
...propsData, ...propsData,
}, },
provide: { provide: {
networkDocumentationPath: 'path/to/docs',
noEnvironmentSvgPath: 'path/to/svg',
threatMonitoringPath: '/threat-monitoring', threatMonitoringPath: '/threat-monitoring',
projectId: '21', projectId: '21',
...provide, ...provide,
...@@ -51,6 +62,8 @@ describe('NetworkPolicyEditor component', () => { ...@@ -51,6 +62,8 @@ describe('NetworkPolicyEditor component', () => {
}); });
}; };
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findPreview = () => wrapper.findComponent(PolicyPreview); const findPreview = () => wrapper.findComponent(PolicyPreview);
const findAddRuleButton = () => wrapper.findByTestId('add-rule'); const findAddRuleButton = () => wrapper.findByTestId('add-rule');
const findYAMLParsingAlert = () => wrapper.findByTestId('parsing-alert'); const findYAMLParsingAlert = () => wrapper.findByTestId('parsing-alert');
...@@ -92,12 +105,14 @@ describe('NetworkPolicyEditor component', () => { ...@@ -92,12 +105,14 @@ describe('NetworkPolicyEditor component', () => {
}); });
it.each` it.each`
component | status | findComponent | state component | status | findComponent | state
${'policy alert picker'} | ${'does display'} | ${findPolicyAlertPicker} | ${true} ${'policy alert picker'} | ${'does display'} | ${findPolicyAlertPicker} | ${true}
${'policy name input'} | ${'does display'} | ${findPolicyName} | ${true} ${'policy name input'} | ${'does display'} | ${findPolicyName} | ${true}
${'add rule button'} | ${'does display'} | ${findAddRuleButton} | ${true} ${'add rule button'} | ${'does display'} | ${findAddRuleButton} | ${true}
${'policy preview'} | ${'does display'} | ${findPreview} | ${true} ${'policy preview'} | ${'does display'} | ${findPreview} | ${true}
${'parsing error alert'} | ${'does not display'} | ${findYAMLParsingAlert} | ${false} ${'parsing error alert'} | ${'does not display'} | ${findYAMLParsingAlert} | ${false}
${'loading icon'} | ${'does not display'} | ${findLoadingIcon} | ${false}
${'no environment empty state'} | ${'does not display'} | ${findEmptyState} | ${false}
`('$status the $component', async ({ findComponent, state }) => { `('$status the $component', async ({ findComponent, state }) => {
expect(findComponent().exists()).toBe(state); expect(findComponent().exists()).toBe(state);
}); });
...@@ -140,7 +155,7 @@ describe('NetworkPolicyEditor component', () => { ...@@ -140,7 +155,7 @@ describe('NetworkPolicyEditor component', () => {
await findPolicyEditorLayout().vm.$emit('save-policy', EDITOR_MODE_YAML); await findPolicyEditorLayout().vm.$emit('save-policy', EDITOR_MODE_YAML);
expect(store.dispatch).toHaveBeenCalledWith('networkPolicies/createPolicy', { expect(store.dispatch).toHaveBeenCalledWith('networkPolicies/createPolicy', {
environmentId: -1, environmentId: 1,
policy: { manifest: mockL7Manifest }, policy: { manifest: mockL7Manifest },
}); });
expect(redirectTo).toHaveBeenCalledWith('/threat-monitoring'); expect(redirectTo).toHaveBeenCalledWith('/threat-monitoring');
...@@ -244,7 +259,7 @@ describe('NetworkPolicyEditor component', () => { ...@@ -244,7 +259,7 @@ describe('NetworkPolicyEditor component', () => {
it('creates policy and redirects to a threat monitoring path', async () => { it('creates policy and redirects to a threat monitoring path', async () => {
await findPolicyEditorLayout().vm.$emit('save-policy'); await findPolicyEditorLayout().vm.$emit('save-policy');
expect(store.dispatch).toHaveBeenCalledWith('networkPolicies/createPolicy', { expect(store.dispatch).toHaveBeenCalledWith('networkPolicies/createPolicy', {
environmentId: -1, environmentId: 1,
policy: { manifest: toYaml(wrapper.vm.policy) }, policy: { manifest: toYaml(wrapper.vm.policy) },
}); });
expect(redirectTo).toHaveBeenCalledWith('/threat-monitoring'); expect(redirectTo).toHaveBeenCalledWith('/threat-monitoring');
...@@ -253,9 +268,7 @@ describe('NetworkPolicyEditor component', () => { ...@@ -253,9 +268,7 @@ describe('NetworkPolicyEditor component', () => {
describe('given there is a createPolicy error', () => { describe('given there is a createPolicy error', () => {
beforeEach(() => { beforeEach(() => {
factory({ factory({
state: { updatedStore: { networkPolicies: { errorUpdatingPolicy: true }, ...defaultStore },
errorUpdatingPolicy: true,
},
}); });
}); });
...@@ -289,7 +302,7 @@ describe('NetworkPolicyEditor component', () => { ...@@ -289,7 +302,7 @@ describe('NetworkPolicyEditor component', () => {
it('updates existing policy and redirects to a threat monitoring path', async () => { it('updates existing policy and redirects to a threat monitoring path', async () => {
await findPolicyEditorLayout().vm.$emit('save-policy'); await findPolicyEditorLayout().vm.$emit('save-policy');
expect(store.dispatch).toHaveBeenCalledWith('networkPolicies/updatePolicy', { expect(store.dispatch).toHaveBeenCalledWith('networkPolicies/updatePolicy', {
environmentId: -1, environmentId: 1,
policy: { name: 'policy', manifest: toYaml(wrapper.vm.policy) }, policy: { name: 'policy', manifest: toYaml(wrapper.vm.policy) },
}); });
expect(redirectTo).toHaveBeenCalledWith('/threat-monitoring'); expect(redirectTo).toHaveBeenCalledWith('/threat-monitoring');
...@@ -298,12 +311,8 @@ describe('NetworkPolicyEditor component', () => { ...@@ -298,12 +311,8 @@ describe('NetworkPolicyEditor component', () => {
describe('given there is a updatePolicy error', () => { describe('given there is a updatePolicy error', () => {
beforeEach(() => { beforeEach(() => {
factory({ factory({
propsData: { propsData: { existingPolicy: { name: 'policy', manifest } },
existingPolicy: { name: 'policy', manifest }, updatedStore: { networkPolicies: { errorUpdatingPolicy: true }, ...defaultStore },
},
state: {
errorUpdatingPolicy: true,
},
}); });
}); });
...@@ -318,7 +327,7 @@ describe('NetworkPolicyEditor component', () => { ...@@ -318,7 +327,7 @@ describe('NetworkPolicyEditor component', () => {
await findPolicyEditorLayout().vm.$emit('remove-policy'); await findPolicyEditorLayout().vm.$emit('remove-policy');
expect(store.dispatch).toHaveBeenCalledWith('networkPolicies/deletePolicy', { expect(store.dispatch).toHaveBeenCalledWith('networkPolicies/deletePolicy', {
environmentId: -1, environmentId: 1,
policy: { name: 'policy', manifest: toYaml(wrapper.vm.policy) }, policy: { name: 'policy', manifest: toYaml(wrapper.vm.policy) },
}); });
expect(redirectTo).toHaveBeenCalledWith('/threat-monitoring'); expect(redirectTo).toHaveBeenCalledWith('/threat-monitoring');
...@@ -329,7 +338,7 @@ describe('NetworkPolicyEditor component', () => { ...@@ -329,7 +338,7 @@ describe('NetworkPolicyEditor component', () => {
it('adds a policy annotation on alert addition', async () => { it('adds a policy annotation on alert addition', async () => {
await modifyPolicyAlert({ isAlertEnabled: true }); await modifyPolicyAlert({ isAlertEnabled: true });
expect(store.dispatch).toHaveBeenLastCalledWith('networkPolicies/createPolicy', { expect(store.dispatch).toHaveBeenLastCalledWith('networkPolicies/createPolicy', {
environmentId: -1, environmentId: 1,
policy: { policy: {
manifest: expect.stringContaining("app.gitlab.com/alert: 'true'"), manifest: expect.stringContaining("app.gitlab.com/alert: 'true'"),
}, },
...@@ -339,11 +348,42 @@ describe('NetworkPolicyEditor component', () => { ...@@ -339,11 +348,42 @@ describe('NetworkPolicyEditor component', () => {
it('removes a policy annotation on alert removal', async () => { it('removes a policy annotation on alert removal', async () => {
await modifyPolicyAlert({ isAlertEnabled: false }); await modifyPolicyAlert({ isAlertEnabled: false });
expect(store.dispatch).toHaveBeenLastCalledWith('networkPolicies/createPolicy', { expect(store.dispatch).toHaveBeenLastCalledWith('networkPolicies/createPolicy', {
environmentId: -1, environmentId: 1,
policy: { policy: {
manifest: expect.not.stringContaining("app.gitlab.com/alert: 'true'"), manifest: expect.not.stringContaining("app.gitlab.com/alert: 'true'"),
}, },
}); });
}); });
}); });
describe('when loading environments', () => {
beforeEach(() => {
factory({
updatedStore: { threatMonitoring: { environments: [], isLoadingEnvironments: true } },
});
});
it.each`
component | status | findComponent | state
${'loading icon'} | ${'does display'} | ${findLoadingIcon} | ${true}
${'policy editor layout'} | ${'does not display'} | ${findPolicyEditorLayout} | ${false}
${'no environment empty state'} | ${'does not display'} | ${findEmptyState} | ${false}
`('$status the $component', ({ findComponent, state }) => {
expect(findComponent().exists()).toBe(state);
});
});
describe('when no environments are configured', () => {
beforeEach(() => {
factory({ updatedStore: { threatMonitoring: { environments: [] } } });
});
it.each`
component | status | findComponent | state
${'loading icon'} | ${'does display'} | ${findLoadingIcon} | ${false}
${'policy editor layout'} | ${'does not display'} | ${findPolicyEditorLayout} | ${false}
${'no environment empty state'} | ${'does not display'} | ${findEmptyState} | ${true}
`('$status the $component', ({ findComponent, state }) => {
expect(findComponent().exists()).toBe(state);
});
});
}); });
...@@ -46,14 +46,17 @@ describe('PolicyEditor component', () => { ...@@ -46,14 +46,17 @@ describe('PolicyEditor component', () => {
beforeEach(factory); beforeEach(factory);
it.each` it.each`
component | status | findComponent | state component | status | findComponent | state
${'environment picker'} | ${'does display'} | ${findEnvironmentPicker} | ${true} ${'environment picker'} | ${'does display'} | ${findEnvironmentPicker} | ${true}
${'NetworkPolicyEditor component'} | ${'does display'} | ${findNeworkPolicyEditor} | ${true} ${'alert'} | ${'does not display'} | ${findAlert} | ${false}
${'alert'} | ${'does not display'} | ${findAlert} | ${false}
`('$status the $component', ({ findComponent, state }) => { `('$status the $component', ({ findComponent, state }) => {
expect(findComponent().exists()).toBe(state); expect(findComponent().exists()).toBe(state);
}); });
it('renders the network policy editor component', () => {
expect(findNeworkPolicyEditor().props('existingPolicy')).toBe(null);
});
it('renders the disabled form select', () => { it('renders the disabled form select', () => {
const formSelect = findFormSelect(); const formSelect = findFormSelect();
expect(formSelect.exists()).toBe(true); expect(formSelect.exists()).toBe(true);
......
...@@ -44,6 +44,8 @@ RSpec.describe Projects::Security::PoliciesHelper do ...@@ -44,6 +44,8 @@ RSpec.describe Projects::Security::PoliciesHelper do
configure_agent_help_path: kind_of(String), configure_agent_help_path: kind_of(String),
create_agent_help_path: kind_of(String), create_agent_help_path: kind_of(String),
environments_endpoint: kind_of(String), environments_endpoint: kind_of(String),
network_documentation_path: kind_of(String),
no_environment_svg_path: kind_of(String),
project_path: project.full_path, project_path: project.full_path,
project_id: project.id, project_id: project.id,
threat_monitoring_path: kind_of(String), threat_monitoring_path: kind_of(String),
......
...@@ -168,111 +168,6 @@ RSpec.describe Banzai::Filter::References::IterationReferenceFilter do ...@@ -168,111 +168,6 @@ RSpec.describe Banzai::Filter::References::IterationReferenceFilter do
end end
end end
shared_examples 'linking to a iteration as the entire link' do
let(:unquoted_reference) { "#{Iteration.reference_prefix}#{iteration.name}" }
let(:link) { urls.iteration_url(iteration) }
let(:link_reference) { %Q{<a href="#{link}">#{link}</a>} }
it 'replaces the link text with the iteration reference' do
doc = reference_filter("See #{link}")
expect(doc.css('a').first.text).to eq(unquoted_reference)
end
it 'includes a data-project attribute' do
doc = reference_filter("Iteration #{link_reference}")
link = doc.css('a').first
expect(link).to have_attribute('data-project')
expect(link.attr('data-project')).to eq project.id.to_s
end
it 'includes a data-iteration attribute' do
doc = reference_filter("See #{link_reference}")
link = doc.css('a').first
expect(link).to have_attribute('data-iteration')
expect(link.attr('data-iteration')).to eq iteration.id.to_s
end
end
shared_examples 'cross-project / cross-namespace complete reference' do
let(:namespace) { create(:namespace) }
let(:another_project) { create(:project, :public, namespace: namespace) }
let(:iteration) { create(:iteration, project: another_project) }
let(:reference) { "#{another_project.full_path}*iteration:#{iteration.iid}" }
let!(:result) { reference_filter("See #{reference}") }
it 'points to referenced project iteration page' do
expect(result.css('a').first.attr('href'))
.to eq(urls.project_iteration_url(another_project, iteration))
end
it 'link has valid text' do
doc = reference_filter("See (#{reference}.)")
expect(doc.css('a').first.text)
.to eq("#{iteration.reference_link_text} in #{another_project.full_path}")
end
it 'has valid text' do
doc = reference_filter("See (#{reference}.)")
expect(doc.text)
.to eq("See (#{iteration.reference_link_text} in #{another_project.full_path}.)")
end
it 'escapes the name attribute' do
allow_next_instance_of(Iteration) do |instance|
allow(instance).to receive(:title).and_return(%{"></a>whatever<a title="})
end
doc = reference_filter("See #{reference}")
expect(doc.css('a').first.text)
.to eq "#{iteration.reference_link_text} in #{another_project.full_path}"
end
end
shared_examples 'cross project shorthand reference' do
let(:namespace) { create(:namespace) }
let(:project) { create(:project, :public, namespace: namespace) }
let(:another_project) { create(:project, :public, namespace: namespace) }
let(:iteration) { create(:iteration, project: another_project) }
let(:reference) { "#{another_project.path}*iteration:#{iteration.iid}" }
let!(:result) { reference_filter("See #{reference}") }
it 'points to referenced project iteration page' do
expect(result.css('a').first.attr('href')).to eq urls
.project_iteration_url(another_project, iteration)
end
it 'link has valid text' do
doc = reference_filter("See (#{reference}.)")
expect(doc.css('a').first.text)
.to eq("#{iteration.reference_link_text} in #{another_project.path}")
end
it 'has valid text' do
doc = reference_filter("See (#{reference}.)")
expect(doc.text)
.to eq("See (#{iteration.reference_link_text} in #{another_project.path}.)")
end
it 'escapes the name attribute' do
allow_next_instance_of(Iteration) do |instance|
allow(instance).to receive(:title).and_return(%{"></a>whatever<a title="})
end
doc = reference_filter("See #{reference}")
expect(doc.css('a').first.text)
.to eq "#{iteration.reference_link_text} in #{another_project.path}"
end
end
shared_examples 'references with HTML entities' do shared_examples 'references with HTML entities' do
before do before do
iteration.update!(title: '&lt;html&gt;') iteration.update!(title: '&lt;html&gt;')
...@@ -301,11 +196,16 @@ RSpec.describe Banzai::Filter::References::IterationReferenceFilter do ...@@ -301,11 +196,16 @@ RSpec.describe Banzai::Filter::References::IterationReferenceFilter do
it_behaves_like 'String-based multi-word references in quotes' it_behaves_like 'String-based multi-word references in quotes'
it_behaves_like 'referencing a iteration in a link href' it_behaves_like 'referencing a iteration in a link href'
it_behaves_like 'references with HTML entities' it_behaves_like 'references with HTML entities'
it_behaves_like 'HTML text with references' do it_behaves_like 'HTML text with references' do
let(:resource) { iteration } let(:resource) { iteration }
let(:resource_text) { resource.title } let(:resource_text) { resource.title }
end end
it_behaves_like 'Integer-based references' do
let(:reference) { iteration.to_reference(format: :id) }
end
it 'does not support references by IID' do it 'does not support references by IID' do
doc = reference_filter("See #{Iteration.reference_prefix}#{iteration.iid}") doc = reference_filter("See #{Iteration.reference_prefix}#{iteration.iid}")
...@@ -335,7 +235,7 @@ RSpec.describe Banzai::Filter::References::IterationReferenceFilter do ...@@ -335,7 +235,7 @@ RSpec.describe Banzai::Filter::References::IterationReferenceFilter do
end end
it 'supports parent group references' do it 'supports parent group references' do
# we have to update iterations_cadence group first in order to avoid invallid record # we have to update iterations_cadence group first in order to avoid an invalid record
iteration.iterations_cadence.update_column(:group_id, parent_group.id) iteration.iterations_cadence.update_column(:group_id, parent_group.id)
iteration.update_column(:group_id, parent_group.id) iteration.update_column(:group_id, parent_group.id)
...@@ -399,4 +299,55 @@ RSpec.describe Banzai::Filter::References::IterationReferenceFilter do ...@@ -399,4 +299,55 @@ RSpec.describe Banzai::Filter::References::IterationReferenceFilter do
include_context 'group iterations' include_context 'group iterations'
end end
end end
context 'checking N+1' do
let_it_be(:group) { create(:group) }
let_it_be(:group2) { create(:group, parent: group) }
let_it_be(:iteration) { create(:iteration, group: group) }
let_it_be(:iteration_reference) { iteration.to_reference(format: :name) }
let_it_be(:iteration2) { create(:iteration, group: group) }
let_it_be(:iteration2_reference) { iteration2.to_reference(format: :id) }
let_it_be(:iteration3) { create(:iteration, group: group2) }
let_it_be(:iteration3_reference) { iteration3.to_reference(format: :name) }
it 'does not have N+1 per multiple references per group', :use_sql_query_cache, :aggregate_failures do
max_count = 3
markdown = "#{iteration_reference}"
# warm the cache
reference_filter(markdown)
expect do
reference_filter(markdown)
end.not_to exceed_all_query_limit(max_count)
markdown = "#{iteration_reference} *iteration:\"Not Found\" *iteration:\"Not Found2\" #{iteration2_reference}"
expect do
reference_filter(markdown)
end.not_to exceed_all_query_limit(max_count)
end
it 'has N+1 for multiple unique group references', :use_sql_query_cache do
markdown = "#{iteration_reference}"
max_count = 3
# warm the cache
reference_filter(markdown, { project: nil, group: group2 })
expect do
reference_filter(markdown, { project: nil, group: group2 })
end.not_to exceed_all_query_limit(max_count)
# Since we're not batching iteration queries across groups,
# queries increase when a new group is referenced.
# TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/330359
markdown = "#{iteration_reference} #{iteration2_reference} #{iteration3_reference}"
max_count += 1
expect do
reference_filter(markdown, { project: nil, group: group2 })
end.not_to exceed_all_query_limit(max_count)
end
end
end end
...@@ -23,7 +23,8 @@ module Banzai ...@@ -23,7 +23,8 @@ module Banzai
label_relation = labels.where(title: label_names) label_relation = labels.where(title: label_names)
end end
return Label.none if (relation = [id_relation, label_relation].compact).empty? relation = [id_relation, label_relation].compact
return Label.none if relation.all?(Label.none)
Label.from_union(relation) Label.from_union(relation)
end end
......
...@@ -23,7 +23,8 @@ module Banzai ...@@ -23,7 +23,8 @@ module Banzai
milestone_relation = find_milestones(parent, false).where(name: milestone_names) milestone_relation = find_milestones(parent, false).where(name: milestone_names)
end end
return Milestone.none if (relation = [iid_relation, milestone_relation].compact).empty? relation = [iid_relation, milestone_relation].compact
return Milestone.none if relation.all?(Milestone.none)
Milestone.from_union(relation).includes(:project, :group) Milestone.from_union(relation).includes(:project, :group)
end end
...@@ -116,11 +117,11 @@ module Banzai ...@@ -116,11 +117,11 @@ module Banzai
# We don't support IID lookups because IIDs can clash between # We don't support IID lookups because IIDs can clash between
# group/project milestones and group/subgroup milestones. # group/project milestones and group/subgroup milestones.
params[:group_ids] = self_and_ancestors_ids(parent) unless find_by_iid params[:group_ids] = group_and_ancestors_ids(parent) unless find_by_iid
end end
end end
def self_and_ancestors_ids(parent) def group_and_ancestors_ids(parent)
if group_context?(parent) if group_context?(parent)
parent.self_and_ancestors.select(:id) parent.self_and_ancestors.select(:id)
elsif project_context?(parent) elsif project_context?(parent)
......
...@@ -169,7 +169,11 @@ module Gitlab ...@@ -169,7 +169,11 @@ module Gitlab
when ActiveRecord::StatementInvalid, ActionView::Template::Error when ActiveRecord::StatementInvalid, ActionView::Template::Error
# After connecting to the DB Rails will wrap query errors using this # After connecting to the DB Rails will wrap query errors using this
# class. # class.
connection_error?(error.cause) if (cause = error.cause)
connection_error?(cause)
else
false
end
when *CONNECTION_ERRORS when *CONNECTION_ERRORS
true true
else else
......
...@@ -22203,21 +22203,12 @@ msgstr "" ...@@ -22203,21 +22203,12 @@ msgstr ""
msgid "NetworkPolicies|Policy definition" msgid "NetworkPolicies|Policy definition"
msgstr "" msgstr ""
msgid "NetworkPolicies|Policy description"
msgstr ""
msgid "NetworkPolicies|Policy editor" msgid "NetworkPolicies|Policy editor"
msgstr "" msgstr ""
msgid "NetworkPolicies|Policy preview" msgid "NetworkPolicies|Policy preview"
msgstr "" msgstr ""
msgid "NetworkPolicies|Policy status"
msgstr ""
msgid "NetworkPolicies|Policy type"
msgstr ""
msgid "NetworkPolicies|Rule" msgid "NetworkPolicies|Rule"
msgstr "" msgstr ""
...@@ -29682,6 +29673,9 @@ msgstr "" ...@@ -29682,6 +29673,9 @@ msgstr ""
msgid "SecurityOrchestration|Network" msgid "SecurityOrchestration|Network"
msgstr "" msgstr ""
msgid "SecurityOrchestration|Network Policies can be used to limit which network traffic is allowed between containers inside the cluster."
msgstr ""
msgid "SecurityOrchestration|New policy" msgid "SecurityOrchestration|New policy"
msgstr "" msgstr ""
...@@ -29691,9 +29685,18 @@ msgstr "" ...@@ -29691,9 +29685,18 @@ msgstr ""
msgid "SecurityOrchestration|Policies" msgid "SecurityOrchestration|Policies"
msgstr "" msgstr ""
msgid "SecurityOrchestration|Policy description"
msgstr ""
msgid "SecurityOrchestration|Policy editor" msgid "SecurityOrchestration|Policy editor"
msgstr "" msgstr ""
msgid "SecurityOrchestration|Policy status"
msgstr ""
msgid "SecurityOrchestration|Policy type"
msgstr ""
msgid "SecurityOrchestration|Scan Execution" msgid "SecurityOrchestration|Scan Execution"
msgstr "" msgstr ""
......
...@@ -283,6 +283,12 @@ RSpec.describe Gitlab::Database::LoadBalancing::LoadBalancer, :request_store do ...@@ -283,6 +283,12 @@ RSpec.describe Gitlab::Database::LoadBalancing::LoadBalancer, :request_store do
expect(lb.connection_error?(error)).to eq(false) expect(lb.connection_error?(error)).to eq(false)
end end
it 'returns false for ActiveRecord errors without a cause' do
error = ActiveRecord::RecordNotUnique.new
expect(lb.connection_error?(error)).to eq(false)
end
end end
describe '#serialization_failure?' do describe '#serialization_failure?' do
......
...@@ -931,6 +931,10 @@ RSpec.describe ApplicationSetting do ...@@ -931,6 +931,10 @@ RSpec.describe ApplicationSetting do
throttle_unauthenticated_packages_api_period_in_seconds throttle_unauthenticated_packages_api_period_in_seconds
throttle_authenticated_packages_api_requests_per_period throttle_authenticated_packages_api_requests_per_period
throttle_authenticated_packages_api_period_in_seconds throttle_authenticated_packages_api_period_in_seconds
throttle_unauthenticated_files_api_requests_per_period
throttle_unauthenticated_files_api_period_in_seconds
throttle_authenticated_files_api_requests_per_period
throttle_authenticated_files_api_period_in_seconds
] ]
end end
......
...@@ -362,6 +362,32 @@ RSpec.describe ApplicationSettings::UpdateService do ...@@ -362,6 +362,32 @@ RSpec.describe ApplicationSettings::UpdateService do
end end
end end
context 'when files API rate limits are passed' do
let(:params) do
{
throttle_unauthenticated_files_api_enabled: 1,
throttle_unauthenticated_files_api_period_in_seconds: 500,
throttle_unauthenticated_files_api_requests_per_period: 20,
throttle_authenticated_files_api_enabled: 1,
throttle_authenticated_files_api_period_in_seconds: 600,
throttle_authenticated_files_api_requests_per_period: 10
}
end
it 'updates files API throttle settings' do
subject.execute
application_settings.reload
expect(application_settings.throttle_unauthenticated_files_api_enabled).to be_truthy
expect(application_settings.throttle_unauthenticated_files_api_period_in_seconds).to eq(500)
expect(application_settings.throttle_unauthenticated_files_api_requests_per_period).to eq(20)
expect(application_settings.throttle_authenticated_files_api_enabled).to be_truthy
expect(application_settings.throttle_authenticated_files_api_period_in_seconds).to eq(600)
expect(application_settings.throttle_authenticated_files_api_requests_per_period).to eq(10)
end
end
context 'when issues_create_limit is passed' do context 'when issues_create_limit is passed' do
let(:params) do let(:params) do
{ {
......
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