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
:throttle_authenticated_packages_api_enabled,
:throttle_authenticated_packages_api_period_in_seconds,
: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_period_in_seconds,
:throttle_unauthenticated_requests_per_period,
:throttle_unauthenticated_packages_api_enabled,
:throttle_unauthenticated_packages_api_period_in_seconds,
: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_period_in_seconds,
:throttle_protected_paths_requests_per_period,
......
......@@ -479,6 +479,14 @@ class ApplicationSetting < ApplicationRecord
presence: true,
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,
presence: true,
numericality: { only_integer: true, greater_than: 0 }
......@@ -503,6 +511,14 @@ class ApplicationSetting < ApplicationRecord
presence: true,
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,
presence: true,
numericality: { only_integer: true, greater_than: 0 }
......
......@@ -167,6 +167,9 @@ module ApplicationSettingImplementation
throttle_authenticated_packages_api_enabled: false,
throttle_authenticated_packages_api_period_in_seconds: 15,
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_per_period: 3600,
throttle_incident_management_notification_period_in_seconds: 3600,
......@@ -179,6 +182,9 @@ module ApplicationSettingImplementation
throttle_unauthenticated_packages_api_enabled: false,
throttle_unauthenticated_packages_api_period_in_seconds: 15,
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,
two_factor_grace_period: 48,
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 (
encrypted_customers_dot_jwt_signing_key bytea,
encrypted_customers_dot_jwt_signing_key_iv bytea,
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_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)),
......@@ -15,7 +15,7 @@ Available resources for the [GitLab REST API](index.md) can be grouped in the fo
See also:
- [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).
## Project resources
......
......@@ -8,7 +8,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w
## 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
GET /deploy_keys
......@@ -74,7 +75,7 @@ Example response:
]
```
## Single deploy key
## Get a single deploy key
Get a single key.
......@@ -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
group, this can be achieved quite easily with the API.
If you want to add the same deploy key to multiple projects in the same
group, this can be achieved with the API.
First, find the ID of the projects you're interested in, by either listing all
projects:
......
......@@ -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
---
# 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
other services to access GitLab resources on user's behalf.
......@@ -15,9 +15,9 @@ other services, see the [OAuth2 authentication service provider](../integration/
documentation. This functionality is based on the
[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):**
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:
server-side apps.
- **Implicit grant:** Originally designed for user-agent only apps, such as
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.
- **Resource owner password credentials:** To be used **only** for securely
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
Implicit grant and Resource Owner Password Credentials flows.
it will be deprecated in the next OAuth specification version.
Implicit grant and Resource Owner Password Credentials flows. It will be deprecated in the next OAuth specification version.
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.
......@@ -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 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)
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
......@@ -123,7 +122,7 @@ Before starting the flow, generate the `STATE`, the `CODE_VERIFIER` and the `COD
"created_at": 1607635748
}
```
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:
- 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
```
Example response:
```json
{
"access_token": "c97d1fe52119f38c7f67f0a14db68d60caa35ddc86fd12401718b649dcfa9c68",
......@@ -203,7 +202,7 @@ be used as a CSRF token.
"created_at": 1607635748
}
```
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:
- Invalidates the existing `access_token` and `refresh_token`.
......@@ -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
`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`
immediately as a result of the authorization request. The flow does not use
the client secret or the authorization code because all of the application code
and storage is easily accessible on client browsers and mobile devices.
immediately as a result of the authorization request. The flow does not use the
client secret or the authorization code, as the application
code and storage is accessible on client browsers and mobile devices.
To request the access token, you should redirect the user to the
`/oauth/authorize` endpoint using `token` response type:
......@@ -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"
```
## 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.
For more information, see [`/oauth/token/info`](https://github.com/doorkeeper-gem/doorkeeper/wiki/API-endpoint-descriptions-and-examples#get----oauthtokeninfo).
To verify the details of a token, use the `token/info` endpoint provided by the
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:
......@@ -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.
## 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:
- The GitLab [Container registry](../user/packages/container_registry/index.md#authenticate-with-the-container-registry).
......
......@@ -108,13 +108,14 @@ V1.
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
the symbol `%24` (see example below).
| Attribute | Type | Required | Description |
| -------------- | ------ | -------- | ----------- |
| `id` | string | yes | The ID or full path of the group. |
| `package_name` | string | yes | The name of the package. |
Note the `$` symbol in the URL. When making requests, you may need the
URL-encoded version of the symbol `%24`. Refer to the example after
the table:
| Attribute | Type | Required | Description |
|----------------|--------|----------|---------------------------------------------------------------------------------------|
| `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). |
```shell
......
......@@ -58,11 +58,11 @@ Upload a package.
PUT projects/:id/packages/npm/:package_name
```
| Attribute | Type | Required | Description |
| ----------------- | ------ | -------- | ----------- |
| `id` | string | yes | The ID or full path of the project. |
| `package_name` | string | yes | The name of the package. |
| `versions` | string | yes | Package version info. |
| Attribute | Type | Required | Description |
|----------------|--------|----------|-------------------------------------|
| `id` | string | yes | The ID or full path of the project. |
| `package_name` | string | yes | The name of the package. |
| `versions` | string | yes | Package version information. |
```shell
curl --request PUT
......
......@@ -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
```
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
......@@ -106,7 +107,7 @@ GET projects/:id/packages/pypi/files/:sha256/:file_identifier
| --------- | ---- | -------- | ----------- |
| `id` | string | yes | The ID or full path of the project. |
| `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
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:
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
......
......@@ -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)
- [Project access tokens](../../api/index.md#personalproject-access-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
......
<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 { removeUnnecessaryDashes } from 'ee/threat_monitoring/utils';
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 DimDisableContainer from '../dim_disable_container.vue';
import PolicyActionPicker from '../policy_action_picker.vue';
......@@ -24,13 +33,19 @@ import PolicyRuleBuilder from './policy_rule_builder.vue';
export default {
EDITOR_MODES,
i18n: {
toggleLabel: s__('NetworkPolicies|Policy status'),
toggleLabel: s__('SecurityOrchestration|Policy status'),
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: {
GlEmptyState,
GlFormGroup,
GlFormInput,
GlFormTextarea,
GlLoadingIcon,
GlToggle,
GlButton,
GlAlert,
......@@ -41,7 +56,7 @@ export default {
PolicyEditorLayout,
DimDisableContainer,
},
inject: ['threatMonitoringPath', 'projectId'],
inject: ['networkDocumentationPath', 'noEnvironmentSvgPath', 'projectId', 'threatMonitoringPath'],
props: {
existingPolicy: {
type: Object,
......@@ -71,6 +86,9 @@ export default {
};
},
computed: {
hasEnvironment() {
return Boolean(this.environments.length);
},
humanizedPolicy() {
return this.policy.error ? null : humanizeNetworkPolicy(this.policy);
},
......@@ -80,7 +98,11 @@ export default {
policyYaml() {
return this.hasParsingError ? '' : toYaml(this.policy);
},
...mapState('threatMonitoring', ['currentEnvironmentId']),
...mapState('threatMonitoring', [
'currentEnvironmentId',
'environments',
'isLoadingEnvironments',
]),
...mapState('networkPolicies', [
'isUpdatingPolicy',
'isRemovingPolicy',
......@@ -159,7 +181,9 @@ export default {
</script>
<template>
<gl-loading-icon v-if="isLoadingEnvironments" size="lg" />
<policy-editor-layout
v-else-if="hasEnvironment"
:is-editing="isEditing"
:is-removing-policy="isRemovingPolicy"
:is-updating-policy="isUpdatingPolicy"
......@@ -255,4 +279,12 @@ export default {
</dim-disable-container>
</template>
</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>
......@@ -79,10 +79,10 @@ export default {
{{ error }}
</gl-alert>
<header class="gl-pb-5">
<h3>{{ s__('NetworkPolicies|Policy description') }}</h3>
<h3>{{ s__('SecurityOrchestration|Policy description') }}</h3>
</header>
<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
id="policyType"
:value="policyOptions.value"
......
......@@ -20,7 +20,9 @@ export default () => {
environmentsEndpoint,
configureAgentHelpPath,
createAgentHelpPath,
networkDocumentationPath,
networkPoliciesEndpoint,
noEnvironmentSvgPath,
threatMonitoringPath,
policy,
policyType,
......@@ -58,6 +60,8 @@ export default () => {
createAgentHelpPath,
disableScanExecutionUpdate: parseBoolean(disableScanExecutionUpdate),
policyType,
networkDocumentationPath,
noEnvironmentSvgPath,
projectId,
projectPath,
threatMonitoringPath,
......
......@@ -20,7 +20,9 @@ class IterationsFinder
@current_user = current_user
end
def execute
def execute(skip_authorization: false)
@skip_authorization = skip_authorization
items = Iteration.all
items = by_id(items)
items = by_iid(items)
......@@ -36,8 +38,10 @@ class IterationsFinder
private
attr_reader :skip_authorization
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)
end
......
......@@ -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'),
environments_endpoint: project_environments_path(project),
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_type: policy_type,
project_path: project.full_path,
......
......@@ -8,10 +8,56 @@ module EE
module IterationReferenceFilter
include ::Gitlab::Utils::StrongMemoize
def find_object(parent, id)
return unless valid_context?(parent)
def parent_records(parent, ids)
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
def valid_context?(parent)
......@@ -37,12 +83,14 @@ module EE
return super(text, pattern) if pattern != ::Iteration.reference_pattern
iterations = {}
unescaped_html = unescape_html_entities(text).gsub(pattern) do |match|
iteration = parse_and_find_iteration($~[:project], $~[:namespace], $~[:iteration_id], $~[:iteration_name])
if iteration
iterations[iteration.id] = yield match, iteration.id, $~[:project], $~[:namespace], $~
"#{::Banzai::Filter::References::AbstractReferenceFilter::REFERENCE_PLACEHOLDER}#{iteration.id}"
unescaped_html = unescape_html_entities(text).gsub(pattern).with_index do |match, index|
ident = identifier($~)
iteration = yield match, ident, $~[:project], $~[:namespace], $~
if iteration != match
iterations[index] = iteration
"#{::Banzai::Filter::References::AbstractReferenceFilter::REFERENCE_PLACEHOLDER}#{index}"
else
match
end
......@@ -53,35 +101,16 @@ module EE
escape_with_placeholders(unescaped_html, iterations)
end
def parse_and_find_iteration(project_ref, namespace_ref, iteration_id, iteration_name)
project_path = reference_cache.full_project_path(namespace_ref, project_ref)
# 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 find_iterations(parent, ids: nil, names: nil)
finder_params = iteration_finder_params(parent, ids: ids, names: names)
def iteration_params(id, name)
if name
{ name: name.tr('"', '') }
else
{ id: id.to_i }
end
IterationsFinder.new(user, finder_params).execute(skip_authorization: true)
end
# rubocop: disable CodeReuse/ActiveRecord
def find_iteration(parent, params)
::Iteration.for_projects_and_groups(project_ids(parent), group_and_ancestors_ids(parent)).find_by(**params)
end
# rubocop: enable CodeReuse/ActiveRecord
def iteration_finder_params(parent, ids: nil, names: nil)
parms = ids.present? ? { id: ids } : { title: names }
def project_ids(parent)
parent.id if project_context?(parent)
{ parent: parent, include_ancestors: true }.merge(parms)
end
def group_and_ancestors_ids(parent)
......@@ -94,8 +123,8 @@ module EE
def url_for_object(iteration, _parent)
::Gitlab::Routing
.url_helpers
.iteration_url(iteration, only_path: context[:only_path])
.url_helpers
.iteration_url(iteration, only_path: context[:only_path])
end
def object_link_text(object, matches)
......@@ -112,6 +141,14 @@ module EE
def object_link_title(_object, _matches)
'Iteration'
end
def parent
project || group
end
def requires_unescaping?
true
end
end
end
end
......
......@@ -37,6 +37,14 @@ RSpec.describe IterationsFinder do
expect(subject).to be_empty
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
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 {
RuleDirectionInbound,
......@@ -25,22 +25,33 @@ describe('NetworkPolicyEditor component', () => {
let store;
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();
Object.assign(store.state.threatMonitoring, {
...state,
});
Object.assign(store.state.networkPolicies, {
...state,
store.replaceState({
...store.state,
networkPolicies: {
...store.state.networkPolicies,
...updatedStore.networkPolicies,
},
threatMonitoring: {
...store.state.threatMonitoring,
...updatedStore.threatMonitoring,
},
});
jest.spyOn(store, 'dispatch').mockImplementation(() => Promise.resolve());
wrapper = shallowMountExtended(NetworkPolicyEditor, {
propsData: {
hasEnvironment: true,
...propsData,
},
provide: {
networkDocumentationPath: 'path/to/docs',
noEnvironmentSvgPath: 'path/to/svg',
threatMonitoringPath: '/threat-monitoring',
projectId: '21',
...provide,
......@@ -51,6 +62,8 @@ describe('NetworkPolicyEditor component', () => {
});
};
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findPreview = () => wrapper.findComponent(PolicyPreview);
const findAddRuleButton = () => wrapper.findByTestId('add-rule');
const findYAMLParsingAlert = () => wrapper.findByTestId('parsing-alert');
......@@ -92,12 +105,14 @@ describe('NetworkPolicyEditor component', () => {
});
it.each`
component | status | findComponent | state
${'policy alert picker'} | ${'does display'} | ${findPolicyAlertPicker} | ${true}
${'policy name input'} | ${'does display'} | ${findPolicyName} | ${true}
${'add rule button'} | ${'does display'} | ${findAddRuleButton} | ${true}
${'policy preview'} | ${'does display'} | ${findPreview} | ${true}
${'parsing error alert'} | ${'does not display'} | ${findYAMLParsingAlert} | ${false}
component | status | findComponent | state
${'policy alert picker'} | ${'does display'} | ${findPolicyAlertPicker} | ${true}
${'policy name input'} | ${'does display'} | ${findPolicyName} | ${true}
${'add rule button'} | ${'does display'} | ${findAddRuleButton} | ${true}
${'policy preview'} | ${'does display'} | ${findPreview} | ${true}
${'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 }) => {
expect(findComponent().exists()).toBe(state);
});
......@@ -140,7 +155,7 @@ describe('NetworkPolicyEditor component', () => {
await findPolicyEditorLayout().vm.$emit('save-policy', EDITOR_MODE_YAML);
expect(store.dispatch).toHaveBeenCalledWith('networkPolicies/createPolicy', {
environmentId: -1,
environmentId: 1,
policy: { manifest: mockL7Manifest },
});
expect(redirectTo).toHaveBeenCalledWith('/threat-monitoring');
......@@ -244,7 +259,7 @@ describe('NetworkPolicyEditor component', () => {
it('creates policy and redirects to a threat monitoring path', async () => {
await findPolicyEditorLayout().vm.$emit('save-policy');
expect(store.dispatch).toHaveBeenCalledWith('networkPolicies/createPolicy', {
environmentId: -1,
environmentId: 1,
policy: { manifest: toYaml(wrapper.vm.policy) },
});
expect(redirectTo).toHaveBeenCalledWith('/threat-monitoring');
......@@ -253,9 +268,7 @@ describe('NetworkPolicyEditor component', () => {
describe('given there is a createPolicy error', () => {
beforeEach(() => {
factory({
state: {
errorUpdatingPolicy: true,
},
updatedStore: { networkPolicies: { errorUpdatingPolicy: true }, ...defaultStore },
});
});
......@@ -289,7 +302,7 @@ describe('NetworkPolicyEditor component', () => {
it('updates existing policy and redirects to a threat monitoring path', async () => {
await findPolicyEditorLayout().vm.$emit('save-policy');
expect(store.dispatch).toHaveBeenCalledWith('networkPolicies/updatePolicy', {
environmentId: -1,
environmentId: 1,
policy: { name: 'policy', manifest: toYaml(wrapper.vm.policy) },
});
expect(redirectTo).toHaveBeenCalledWith('/threat-monitoring');
......@@ -298,12 +311,8 @@ describe('NetworkPolicyEditor component', () => {
describe('given there is a updatePolicy error', () => {
beforeEach(() => {
factory({
propsData: {
existingPolicy: { name: 'policy', manifest },
},
state: {
errorUpdatingPolicy: true,
},
propsData: { existingPolicy: { name: 'policy', manifest } },
updatedStore: { networkPolicies: { errorUpdatingPolicy: true }, ...defaultStore },
});
});
......@@ -318,7 +327,7 @@ describe('NetworkPolicyEditor component', () => {
await findPolicyEditorLayout().vm.$emit('remove-policy');
expect(store.dispatch).toHaveBeenCalledWith('networkPolicies/deletePolicy', {
environmentId: -1,
environmentId: 1,
policy: { name: 'policy', manifest: toYaml(wrapper.vm.policy) },
});
expect(redirectTo).toHaveBeenCalledWith('/threat-monitoring');
......@@ -329,7 +338,7 @@ describe('NetworkPolicyEditor component', () => {
it('adds a policy annotation on alert addition', async () => {
await modifyPolicyAlert({ isAlertEnabled: true });
expect(store.dispatch).toHaveBeenLastCalledWith('networkPolicies/createPolicy', {
environmentId: -1,
environmentId: 1,
policy: {
manifest: expect.stringContaining("app.gitlab.com/alert: 'true'"),
},
......@@ -339,11 +348,42 @@ describe('NetworkPolicyEditor component', () => {
it('removes a policy annotation on alert removal', async () => {
await modifyPolicyAlert({ isAlertEnabled: false });
expect(store.dispatch).toHaveBeenLastCalledWith('networkPolicies/createPolicy', {
environmentId: -1,
environmentId: 1,
policy: {
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', () => {
beforeEach(factory);
it.each`
component | status | findComponent | state
${'environment picker'} | ${'does display'} | ${findEnvironmentPicker} | ${true}
${'NetworkPolicyEditor component'} | ${'does display'} | ${findNeworkPolicyEditor} | ${true}
${'alert'} | ${'does not display'} | ${findAlert} | ${false}
component | status | findComponent | state
${'environment picker'} | ${'does display'} | ${findEnvironmentPicker} | ${true}
${'alert'} | ${'does not display'} | ${findAlert} | ${false}
`('$status the $component', ({ findComponent, 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', () => {
const formSelect = findFormSelect();
expect(formSelect.exists()).toBe(true);
......
......@@ -44,6 +44,8 @@ RSpec.describe Projects::Security::PoliciesHelper do
configure_agent_help_path: kind_of(String),
create_agent_help_path: 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_id: project.id,
threat_monitoring_path: kind_of(String),
......
......@@ -168,111 +168,6 @@ RSpec.describe Banzai::Filter::References::IterationReferenceFilter do
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
before do
iteration.update!(title: '&lt;html&gt;')
......@@ -301,11 +196,16 @@ RSpec.describe Banzai::Filter::References::IterationReferenceFilter do
it_behaves_like 'String-based multi-word references in quotes'
it_behaves_like 'referencing a iteration in a link href'
it_behaves_like 'references with HTML entities'
it_behaves_like 'HTML text with references' do
let(:resource) { iteration }
let(:resource_text) { resource.title }
end
it_behaves_like 'Integer-based references' do
let(:reference) { iteration.to_reference(format: :id) }
end
it 'does not support references by IID' do
doc = reference_filter("See #{Iteration.reference_prefix}#{iteration.iid}")
......@@ -335,7 +235,7 @@ RSpec.describe Banzai::Filter::References::IterationReferenceFilter do
end
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.update_column(:group_id, parent_group.id)
......@@ -399,4 +299,55 @@ RSpec.describe Banzai::Filter::References::IterationReferenceFilter do
include_context 'group iterations'
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
......@@ -23,7 +23,8 @@ module Banzai
label_relation = labels.where(title: label_names)
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)
end
......
......@@ -23,7 +23,8 @@ module Banzai
milestone_relation = find_milestones(parent, false).where(name: milestone_names)
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)
end
......@@ -116,11 +117,11 @@ module Banzai
# We don't support IID lookups because IIDs can clash between
# 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
def self_and_ancestors_ids(parent)
def group_and_ancestors_ids(parent)
if group_context?(parent)
parent.self_and_ancestors.select(:id)
elsif project_context?(parent)
......
......@@ -169,7 +169,11 @@ module Gitlab
when ActiveRecord::StatementInvalid, ActionView::Template::Error
# After connecting to the DB Rails will wrap query errors using this
# class.
connection_error?(error.cause)
if (cause = error.cause)
connection_error?(cause)
else
false
end
when *CONNECTION_ERRORS
true
else
......
......@@ -22203,21 +22203,12 @@ msgstr ""
msgid "NetworkPolicies|Policy definition"
msgstr ""
msgid "NetworkPolicies|Policy description"
msgstr ""
msgid "NetworkPolicies|Policy editor"
msgstr ""
msgid "NetworkPolicies|Policy preview"
msgstr ""
msgid "NetworkPolicies|Policy status"
msgstr ""
msgid "NetworkPolicies|Policy type"
msgstr ""
msgid "NetworkPolicies|Rule"
msgstr ""
......@@ -29682,6 +29673,9 @@ msgstr ""
msgid "SecurityOrchestration|Network"
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"
msgstr ""
......@@ -29691,9 +29685,18 @@ msgstr ""
msgid "SecurityOrchestration|Policies"
msgstr ""
msgid "SecurityOrchestration|Policy description"
msgstr ""
msgid "SecurityOrchestration|Policy editor"
msgstr ""
msgid "SecurityOrchestration|Policy status"
msgstr ""
msgid "SecurityOrchestration|Policy type"
msgstr ""
msgid "SecurityOrchestration|Scan Execution"
msgstr ""
......
......@@ -283,6 +283,12 @@ RSpec.describe Gitlab::Database::LoadBalancing::LoadBalancer, :request_store do
expect(lb.connection_error?(error)).to eq(false)
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
describe '#serialization_failure?' do
......
......@@ -931,6 +931,10 @@ RSpec.describe ApplicationSetting do
throttle_unauthenticated_packages_api_period_in_seconds
throttle_authenticated_packages_api_requests_per_period
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
......
......@@ -362,6 +362,32 @@ RSpec.describe ApplicationSettings::UpdateService do
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
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