Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
3e64e1af8d
commit
0426ca208d
|
@ -145,7 +145,7 @@ export default {
|
|||
},
|
||||
setScope(scope) {
|
||||
this.scope = scope;
|
||||
this.resetPolling();
|
||||
this.moveToPage(1);
|
||||
},
|
||||
movePage(direction) {
|
||||
this.moveToPage(this.pageInfo[`${direction}Page`]);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlSkeletonLoader } from '@gitlab/ui';
|
||||
import { GlButton, GlSkeletonLoader } from '@gitlab/ui';
|
||||
import createFlash from '~/flash';
|
||||
import { __ } from '~/locale';
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
|
@ -29,6 +29,7 @@ export default {
|
|||
statusIcon,
|
||||
GlSkeletonLoader,
|
||||
ActionsButton,
|
||||
GlButton,
|
||||
},
|
||||
mixins: [glFeatureFlagMixin(), mergeRequestQueryVariablesMixin],
|
||||
props: {
|
||||
|
@ -53,6 +54,9 @@ export default {
|
|||
isLoading() {
|
||||
return this.glFeatures.mergeRequestWidgetGraphql && this.$apollo.queries.state.loading;
|
||||
},
|
||||
showRebaseWithoutCi() {
|
||||
return this.glFeatures?.rebaseWithoutCiUi;
|
||||
},
|
||||
rebaseInProgress() {
|
||||
if (this.glFeatures.mergeRequestWidgetGraphql) {
|
||||
return this.state.rebaseInProgress;
|
||||
|
@ -196,8 +200,18 @@ export default {
|
|||
v-if="!rebaseInProgress && canPushToSourceBranch && !isMakingRequest"
|
||||
class="accept-merge-holder clearfix js-toggle-container accept-action media space-children"
|
||||
>
|
||||
<gl-button
|
||||
v-if="!glFeatures.restructuredMrWidget && !showRebaseWithoutCi"
|
||||
:loading="isMakingRequest"
|
||||
variant="confirm"
|
||||
data-qa-selector="mr_rebase_button"
|
||||
data-testid="standard-rebase-button"
|
||||
@click="rebase"
|
||||
>
|
||||
{{ __('Rebase') }}
|
||||
</gl-button>
|
||||
<actions-button
|
||||
v-if="!glFeatures.restructuredMrWidget"
|
||||
v-if="!glFeatures.restructuredMrWidget && showRebaseWithoutCi"
|
||||
:actions="actions"
|
||||
:selected-key="selectedRebaseAction"
|
||||
variant="confirm"
|
||||
|
|
|
@ -43,6 +43,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
|
|||
push_frontend_feature_flag(:mr_changes_fluid_layout, project, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:mr_attention_requests, project, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:refactor_mr_widgets_extensions, @project, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:rebase_without_ci_ui, @project, default_enabled: :yaml)
|
||||
# Usage data feature flags
|
||||
push_frontend_feature_flag(:users_expanding_widgets_usage_data, @project, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:diff_settings_usage_data, default_enabled: :yaml)
|
||||
|
|
|
@ -17,11 +17,14 @@ class RegistrationsController < Devise::RegistrationsController
|
|||
check_rate_limit!(:user_sign_up, scope: request.ip) if Feature.enabled?(:rate_limit_user_sign_up_endpoint, default_enabled: :yaml)
|
||||
end
|
||||
|
||||
before_action only: [:new] do
|
||||
push_frontend_feature_flag(:gitlab_gtm_datalayer, type: :ops)
|
||||
end
|
||||
|
||||
feature_category :authentication_and_authorization
|
||||
|
||||
def new
|
||||
@resource = build_resource
|
||||
push_frontend_feature_flag(:gitlab_gtm_datalayer, type: :ops)
|
||||
end
|
||||
|
||||
def create
|
||||
|
|
|
@ -6,8 +6,8 @@ module Resolvers
|
|||
type Types::WorkItems::TypeType.connection_type, null: true
|
||||
|
||||
def resolve
|
||||
# This will require a finder in the future when groups get their work item types
|
||||
# All groups use the default types for now
|
||||
# This will require a finder in the future when groups/projects get their work item types
|
||||
# All groups/projects use the default types for now
|
||||
::WorkItems::Type.default.order_by_name_asc
|
||||
end
|
||||
end
|
||||
|
|
|
@ -406,6 +406,11 @@ module Types
|
|||
description: 'Labels available on this project.',
|
||||
resolver: Resolvers::LabelsResolver
|
||||
|
||||
field :work_item_types, Types::WorkItems::TypeType.connection_type,
|
||||
resolver: Resolvers::WorkItems::TypesResolver,
|
||||
description: 'Work item types available to the project.',
|
||||
feature_flag: :work_items
|
||||
|
||||
def avatar_url
|
||||
object.avatar_url(only_path: false)
|
||||
end
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: ci_decompose_for_namespace_monthly_usage_query
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/77952
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/350146
|
||||
milestone: '14.7'
|
||||
type: development
|
||||
group: group::pipeline execution
|
||||
default_enabled: false
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: rebase_without_ci_ui
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/78194
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/350262
|
||||
milestone: '14.7'
|
||||
type: development
|
||||
group: group::pipeline execution
|
||||
default_enabled: false
|
|
@ -0,0 +1,16 @@
|
|||
- name: "Removal of `artifacts:report:cobertura` keyword"
|
||||
announcement_milestone: "14.8"
|
||||
announcement_date: "2022-02-22"
|
||||
removal_milestone: "15.0"
|
||||
removal_date: "2022-06-22"
|
||||
breaking_change: false
|
||||
body: |
|
||||
Currently, test coverage visualizations in GitLab only support Cobertura reports. Starting 15.0, the
|
||||
`artifacts:report:cobertura` keyword will be replaced by
|
||||
[`artifacts:reports:coverage_report`](https://gitlab.com/gitlab-org/gitlab/-/issues/344533). Cobertura will be the
|
||||
only supported report file in 15.0, but this is the first step towards GitLab supporting other report types.
|
||||
|
||||
# The following items are not published on the docs page, but may be used in the future.
|
||||
stage: Verify
|
||||
issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/348980
|
||||
documentation_url: https://docs.gitlab.com/ee/ci/yaml/artifacts_reports.html#artifactsreportscobertura
|
|
@ -490,7 +490,15 @@ The following are useful queries for monitoring Gitaly:
|
|||
|
||||
### Monitor Gitaly Cluster
|
||||
|
||||
To monitor Gitaly Cluster (Praefect), you can use these Prometheus metrics:
|
||||
To monitor Gitaly Cluster (Praefect), you can use these Prometheus metrics. There are two separate metrics
|
||||
endpoints from which metrics can be scraped:
|
||||
|
||||
- The default `/metrics` endpoint.
|
||||
- `/db_metrics`, which contains metrics that require database queries.
|
||||
|
||||
#### Default Prometheus `/metrics` endpoint
|
||||
|
||||
The following metrics are available from the `/metrics` endpoint:
|
||||
|
||||
- `gitaly_praefect_read_distribution`, a counter to track [distribution of reads](#distributed-reads).
|
||||
It has two labels:
|
||||
|
@ -523,6 +531,16 @@ To monitor [strong consistency](#strong-consistency), you can use the following
|
|||
|
||||
You can also monitor the [Praefect logs](../logs.md#praefect-logs).
|
||||
|
||||
#### Database metrics `/db_metrics` endpoint
|
||||
|
||||
The following metrics are available from the `/db_metrics` endpoint:
|
||||
|
||||
- `gitaly_praefect_unavailable_repositories`, the number of repositories that have no healthy, up to date replicas.
|
||||
- `gitaly_praefect_read_only_repositories`, the number of repositories in read-only mode within a virtual storage.
|
||||
This is an older metric that is still available for backwards compatibility reasons. `gitaly_praefect_unavailable_repositories`
|
||||
is a more accurate.
|
||||
- `gitaly_praefect_replication_queue_depth`, the number of jobs in the replication queue.
|
||||
|
||||
## Recover from failure
|
||||
|
||||
Gitaly Cluster can [recover from certain types of failure](recovery.md).
|
||||
|
|
|
@ -20,6 +20,9 @@ Configure Gitaly Cluster using either:
|
|||
|
||||
Smaller GitLab installations may need only [Gitaly itself](index.md).
|
||||
|
||||
To upgrade a Gitaly Cluster, follow the documentation for
|
||||
[zero downtime upgrades](../../update/zero_downtime.md#gitaly-cluster).
|
||||
|
||||
## Requirements
|
||||
|
||||
The minimum recommended configuration for a Gitaly Cluster requires:
|
||||
|
@ -376,8 +379,8 @@ configuration option is set. For more details, consult the PgBouncer documentati
|
|||
|
||||
If there are multiple Praefect nodes:
|
||||
|
||||
- Complete the following steps for **each** node.
|
||||
- Designate one node as the "deploy node", and configure it first.
|
||||
1. Designate one node as the deploy node, and configure it using the following steps.
|
||||
1. Complete the following steps for each additional node.
|
||||
|
||||
To complete this section you need a [configured PostgreSQL server](#postgresql), including:
|
||||
|
||||
|
@ -415,10 +418,21 @@ On the **Praefect** node:
|
|||
|
||||
```ruby
|
||||
praefect['listen_addr'] = '0.0.0.0:2305'
|
||||
```
|
||||
|
||||
1. Configure Prometheus metrics by editing
|
||||
`/etc/gitlab/gitlab.rb`:
|
||||
|
||||
```ruby
|
||||
# Enable Prometheus metrics access to Praefect. You must use firewalls
|
||||
# to restrict access to this address/port.
|
||||
# The default metrics endpoint is /metrics
|
||||
praefect['prometheus_listen_addr'] = '0.0.0.0:9652'
|
||||
|
||||
# Some metrics run queries against the database. Enabling separate database metrics allows
|
||||
# these metrics to be collected when the metrics are
|
||||
# scraped on a separate /db_metrics endpoint.
|
||||
praefect['separate_database_metrics'] = true
|
||||
```
|
||||
|
||||
1. Configure a strong `auth_token` for **Praefect** by editing
|
||||
|
@ -556,8 +570,6 @@ On the **Praefect** node:
|
|||
edit `/etc/gitlab/gitlab.rb`, remember to run `sudo gitlab-ctl reconfigure`
|
||||
again before trying the `sql-ping` command.
|
||||
|
||||
**The steps above must be completed for each Praefect node!**
|
||||
|
||||
#### Enabling TLS support
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitaly/-/issues/1698) in GitLab 13.2.
|
||||
|
|
|
@ -289,6 +289,8 @@ control over how the Pages daemon runs and serves content in your environment.
|
|||
| `use_legacy_storage` | Temporarily-introduced parameter allowing to use legacy domain configuration source and storage. [Removed in 14.3](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/6166). |
|
||||
| `rate_limit_source_ip` | Rate limit per source IP in number of requests per second. Set to `0` to disable this feature. |
|
||||
| `rate_limit_source_ip_burst` | Rate limit per source IP maximum burst allowed per second. |
|
||||
| `rate_limit_domain` | Rate limit per domain in number of requests per second. Set to `0` to disable this feature. |
|
||||
| `rate_limit_domain_burst` | Rate limit per domain maximum burst allowed per second. |
|
||||
|
||||
## Advanced configuration
|
||||
|
||||
|
@ -1077,15 +1079,22 @@ than GitLab to prevent XSS attacks.
|
|||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab-pages/-/issues/631) in GitLab 14.5.
|
||||
|
||||
You can enforce source-IP rate limits to help minimize the risk of a Denial of Service (DoS) attack. GitLab Pages
|
||||
You can enforce rate limits to help minimize the risk of a Denial of Service (DoS) attack. GitLab Pages
|
||||
uses a [token bucket algorithm](https://en.wikipedia.org/wiki/Token_bucket) to enforce rate limiting. By default,
|
||||
requests that exceed the specified limits are reported but not rejected.
|
||||
|
||||
Source-IP rate limits are enforced using the following:
|
||||
GitLab Pages supports the following types of rate limiting:
|
||||
|
||||
- `rate_limit_source_ip`: Set the maximum threshold in number of requests per second. Set to 0 to disable this feature.
|
||||
- `rate_limit_source_ip_burst`: Sets the maximum threshold of number of requests allowed in an initial outburst of requests.
|
||||
- Per `source_ip`. It limits how many requests are allowed from the single client IP address.
|
||||
- Per `domain`. It limits how many requests are allowed per domain hosted on GitLab Pages. It can be a custom domain like `example.com`, or group domain like `group.gitlab.io`.
|
||||
|
||||
Rate limits are enforced using the following:
|
||||
|
||||
- `rate_limit_source_ip`: Set the maximum threshold in number of requests per client IP per second. Set to 0 to disable this feature.
|
||||
- `rate_limit_source_ip_burst`: Sets the maximum threshold of number of requests allowed in an initial outburst of requests per client IP.
|
||||
For example, when you load a web page that loads a number of resources at the same time.
|
||||
- `rate_limit_domain_ip`: Set the maximum threshold in number of requests per hosted pages domain per second. Set to 0 to disable this feature.
|
||||
- `rate_limit_domain_burst`: Sets the maximum threshold of number of requests allowed in an initial outburst of requests per hosted pages domain.
|
||||
|
||||
#### Enable source-IP rate limits
|
||||
|
||||
|
@ -1105,6 +1114,24 @@ Source-IP rate limits are enforced using the following:
|
|||
|
||||
1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
|
||||
|
||||
#### Enable domain rate limits
|
||||
|
||||
1. Set rate limits in `/etc/gitlab/gitlab.rb`:
|
||||
|
||||
```ruby
|
||||
gitlab_pages['rate_limit_domain'] = 1000
|
||||
gitlab_pages['rate_limit_domain_burst'] = 5000
|
||||
```
|
||||
|
||||
1. To reject requests that exceed the specified limits, enable the `FF_ENFORCE_DOMAIN_RATE_LIMITS` feature flag in
|
||||
`/etc/gitlab/gitlab.rb`:
|
||||
|
||||
```ruby
|
||||
gitlab_pages['env'] = {'FF_ENFORCE_DOMAIN_RATE_LIMITS' => 'true'}
|
||||
```
|
||||
|
||||
1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
|
||||
|
||||
<!-- ## Troubleshooting
|
||||
|
||||
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
|
||||
|
|
|
@ -13224,6 +13224,7 @@ Represents vulnerability finding of a security report on the pipeline.
|
|||
| <a id="projectvulnerabilityscanners"></a>`vulnerabilityScanners` | [`VulnerabilityScannerConnection`](#vulnerabilityscannerconnection) | Vulnerability scanners reported on the project vulnerabilities. (see [Connections](#connections)) |
|
||||
| <a id="projectweburl"></a>`webUrl` | [`String`](#string) | Web URL of the project. |
|
||||
| <a id="projectwikienabled"></a>`wikiEnabled` | [`Boolean`](#boolean) | Indicates if Wikis are enabled for the current user. |
|
||||
| <a id="projectworkitemtypes"></a>`workItemTypes` | [`WorkItemTypeConnection`](#workitemtypeconnection) | Work item types available to the project. Available only when feature flag `work_items` is enabled. This flag is disabled by default, because the feature is experimental and is subject to change without notice. (see [Connections](#connections)) |
|
||||
|
||||
#### Fields with arguments
|
||||
|
||||
|
|
|
@ -1669,24 +1669,6 @@ If a feature is moved to another tier:
|
|||
> - [Moved](<link-to-issue>) from GitLab Premium to GitLab Free in 12.0.
|
||||
```
|
||||
|
||||
If a feature is deprecated, include a link to a replacement (when available):
|
||||
|
||||
```markdown
|
||||
> - [Deprecated](<link-to-issue>) in GitLab 11.3. Replaced by [meaningful text](<link-to-appropriate-documentation>).
|
||||
```
|
||||
|
||||
You can also describe the replacement in surrounding text, if available. If the
|
||||
deprecation isn't obvious in existing text, you may want to include a warning:
|
||||
|
||||
```markdown
|
||||
WARNING:
|
||||
This feature was [deprecated](link-to-issue) in GitLab 12.3 and replaced by
|
||||
[Feature name](link-to-feature-documentation).
|
||||
```
|
||||
|
||||
In the first major GitLab version after the feature was deprecated, be sure to
|
||||
remove information about that deprecated feature.
|
||||
|
||||
#### Inline version text
|
||||
|
||||
If you're adding content to an existing topic, you can add version information
|
||||
|
@ -1786,6 +1768,47 @@ To view historical information about a feature, review GitLab
|
|||
[release posts](https://about.gitlab.com/releases/), or search for the issue or
|
||||
merge request where the work was done.
|
||||
|
||||
### Deprecated features
|
||||
|
||||
When a feature is deprecated, add `(DEPRECATED)` to the page title or to
|
||||
the heading of the section documenting the feature, immediately before
|
||||
the tier badge:
|
||||
|
||||
```markdown
|
||||
<!-- Page title example: -->
|
||||
# Feature A (DEPRECATED) **(ALL TIERS)**
|
||||
|
||||
<!-- Doc section example: -->
|
||||
## Feature B (DEPRECATED) **(PREMIUM SELF)**
|
||||
```
|
||||
|
||||
Add the deprecation to the version history note (you can include a link
|
||||
to a replacement when available):
|
||||
|
||||
```markdown
|
||||
> - [Deprecated](<link-to-issue>) in GitLab 11.3. Replaced by [meaningful text](<link-to-appropriate-documentation>).
|
||||
```
|
||||
|
||||
You can also describe the replacement in surrounding text, if available. If the
|
||||
deprecation isn't obvious in existing text, you may want to include a warning:
|
||||
|
||||
```markdown
|
||||
WARNING:
|
||||
This feature was [deprecated](link-to-issue) in GitLab 12.3 and replaced by
|
||||
[Feature name](link-to-feature-documentation).
|
||||
```
|
||||
|
||||
If you add `(DEPRECATED)` to the page's title and the document is linked from the docs
|
||||
navigation, either remove the page from the nav or update the nav item to include the
|
||||
same text before the feature name:
|
||||
|
||||
```yaml
|
||||
- doc_title: (DEPRECATED) Feature A
|
||||
```
|
||||
|
||||
In the first major GitLab version after the feature was deprecated, be sure to
|
||||
remove information about that deprecated feature.
|
||||
|
||||
## Products and features
|
||||
|
||||
Refer to the information in this section when describing products and features
|
||||
|
|
|
@ -767,3 +767,354 @@ In the example above, the `is_admin?` method is overwritten when passing it to t
|
|||
- If you must, be **very** confident that you've sanitized the values correctly.
|
||||
Consider creating an allowlist of values, and validating the user input against that.
|
||||
- When extending classes that use metaprogramming, make sure you don't inadvertently override any method definition safety checks.
|
||||
|
||||
## Working with archive files
|
||||
|
||||
Working with archive files like `zip`, `tar`, `jar`, `war`, `cpio`, `apk`, `rar` and `7z` presents an area where potentially critical security vulnerabilities can sneak into an application.
|
||||
|
||||
### Zip Slip
|
||||
|
||||
In 2018, the security company Snyk [released a blog post](https://snyk.io/research/zip-slip-vulnerability) describing research into a widespread and critical vulnerability present in many libraries and applications which allows an attacker to overwrite arbitrary files on the server file system which, in many cases, can be leveraged to achieve remote code execution. The vulnerability was dubbed Zip Slip.
|
||||
|
||||
A Zip Slip vulnerability happens when an application extracts an archive without validating and sanitizing the filenames inside the archive for directory traversal sequences that change the file location when the file is extracted.
|
||||
|
||||
Example malicious file names:
|
||||
|
||||
- `../../etc/passwd`
|
||||
- `../../root/.ssh/authorized_keys`
|
||||
- `../../etc/gitlab/gitlab.rb`
|
||||
|
||||
If a vulnerable application extracts an archive file with any of these file names, the attacker can overwrite these files with arbitrary content.
|
||||
|
||||
### Insecure archive extraction examples
|
||||
|
||||
#### Ruby
|
||||
|
||||
For zip files, the [rubyzip](https://rubygems.org/gems/rubyzip) Ruby gem is already patched against the Zip Slip vulnerability and will refuse to extract files that try to perform directory traversal, so for this vulnerable example we will extract a `tar.gz` file with `Gem::Package::TarReader`:
|
||||
|
||||
```ruby
|
||||
# Vulnerable tar.gz extraction example!
|
||||
|
||||
begin
|
||||
tar_extract = Gem::Package::TarReader.new(Zlib::GzipReader.open("/tmp/uploaded.tar.gz"))
|
||||
rescue Errno::ENOENT
|
||||
STDERR.puts("archive file does not exist or is not readable")
|
||||
exit(false)
|
||||
end
|
||||
tar_extract.rewind
|
||||
|
||||
tar_extract.each do |entry|
|
||||
next unless entry.file? # Only process files in this example for simplicity.
|
||||
|
||||
destination = "/tmp/extracted/#{entry.full_name}" # Oops! We blindly use the entry file name for the destination.
|
||||
File.open(destination, "wb") do |out|
|
||||
out.write(entry.read)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
#### Go
|
||||
|
||||
```golang
|
||||
// unzip INSECURELY extracts source zip file to destination.
|
||||
func unzip(src, dest string) error {
|
||||
r, err := zip.OpenReader(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
os.MkdirAll(dest, 0750)
|
||||
|
||||
for _, f := range r.File {
|
||||
if f.FileInfo().IsDir() { // Skip directories in this example for simplicity.
|
||||
continue
|
||||
}
|
||||
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
path := filepath.Join(dest, f.Name) // Oops! We blindly use the entry file name for the destination.
|
||||
os.MkdirAll(filepath.Dir(path), f.Mode())
|
||||
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if _, err := io.Copy(f, rc); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
#### Best practices
|
||||
|
||||
Always expand the destination file path by resolving all potential directory traversals and other sequences that can alter the path and refuse extraction if the final destination path does not start with the intended destination directory.
|
||||
|
||||
##### Ruby
|
||||
|
||||
```ruby
|
||||
# tar.gz extraction example with protection against Zip Slip attacks.
|
||||
|
||||
begin
|
||||
tar_extract = Gem::Package::TarReader.new(Zlib::GzipReader.open("/tmp/uploaded.tar.gz"))
|
||||
rescue Errno::ENOENT
|
||||
STDERR.puts("archive file does not exist or is not readable")
|
||||
exit(false)
|
||||
end
|
||||
tar_extract.rewind
|
||||
|
||||
tar_extract.each do |entry|
|
||||
next unless entry.file? # Only process files in this example for simplicity.
|
||||
|
||||
# safe_destination will raise an exception in case of Zip Slip / directory traversal.
|
||||
destination = safe_destination(entry.full_name, "/tmp/extracted")
|
||||
|
||||
File.open(destination, "wb") do |out|
|
||||
out.write(entry.read)
|
||||
end
|
||||
end
|
||||
|
||||
def safe_destination(filename, destination_dir)
|
||||
raise "filename cannot start with '/'" if filename.start_with?("/")
|
||||
|
||||
destination_dir = File.realpath(destination_dir)
|
||||
destination = File.expand_path(filename, destination_dir)
|
||||
|
||||
raise "filename is outside of destination directory" unless
|
||||
destination.start_with?(destination_dir + "/"))
|
||||
|
||||
destination
|
||||
end
|
||||
```
|
||||
|
||||
```ruby
|
||||
# zip extraction example using rubyzip with built-in protection against Zip Slip attacks.
|
||||
require 'zip'
|
||||
|
||||
Zip::File.open("/tmp/uploaded.zip") do |zip_file|
|
||||
zip_file.each do |entry|
|
||||
# Extract entry to /tmp/extracted directory.
|
||||
entry.extract("/tmp/extracted")
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
##### Go
|
||||
|
||||
You are encouraged to use the secure archive utilities provided by [LabSec](https://gitlab.com/gitlab-com/gl-security/appsec/labsec) which will handle Zip Slip and other types of vulnerabilities for you. The LabSec utilities are also context aware which makes it possible to cancel or timeout extractions:
|
||||
|
||||
```golang
|
||||
package main
|
||||
|
||||
import "gitlab-com/gl-security/appsec/labsec/archive/zip"
|
||||
|
||||
func main() {
|
||||
f, err := os.Open("/tmp/uploaded.zip")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := zip.Extract(context.Background(), f, fi.Size(), "/tmp/extracted"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In case the LabSec utilities do not fit your needs, here is an example for extracting a zip file with protection against Zip Slip attacks:
|
||||
|
||||
```golang
|
||||
// unzip extracts source zip file to destination with protection against Zip Slip attacks.
|
||||
func unzip(src, dest string) error {
|
||||
r, err := zip.OpenReader(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
os.MkdirAll(dest, 0750)
|
||||
|
||||
for _, f := range r.File {
|
||||
if f.FileInfo().IsDir() { // Skip directories in this example for simplicity.
|
||||
continue
|
||||
}
|
||||
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
path := filepath.Join(dest, f.Name)
|
||||
|
||||
// Check for Zip Slip / directory traversal
|
||||
if !strings.HasPrefix(path, filepath.Clean(dest) + string(os.PathSeparator)) {
|
||||
return fmt.Errorf("illegal file path: %s", path)
|
||||
}
|
||||
|
||||
os.MkdirAll(filepath.Dir(path), f.Mode())
|
||||
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if _, err := io.Copy(f, rc); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### Symlink attacks
|
||||
|
||||
Symlink attacks makes it possible for an attacker to read the contents of arbitrary files on the server of a vulnerable application. While it is a high-severity vulnerability that can often lead to remote code execution and other critical vulnerabilities, it is only exploitable in scenarios where a vulnerable application accepts archive files from the attacker and somehow displays the extracted contents back to the attacker without any validation or sanitization of symbolic links inside the archive.
|
||||
|
||||
### Insecure archive symlink extraction examples
|
||||
|
||||
#### Ruby
|
||||
|
||||
For zip files, the [rubyzip](https://rubygems.org/gems/rubyzip) Ruby gem is already patched against symlink attacks as it simply ignores symbolic links, so for this vulnerable example we will extract a `tar.gz` file with `Gem::Package::TarReader`:
|
||||
|
||||
```ruby
|
||||
# Vulnerable tar.gz extraction example!
|
||||
|
||||
begin
|
||||
tar_extract = Gem::Package::TarReader.new(Zlib::GzipReader.open("/tmp/uploaded.tar.gz"))
|
||||
rescue Errno::ENOENT
|
||||
STDERR.puts("archive file does not exist or is not readable")
|
||||
exit(false)
|
||||
end
|
||||
tar_extract.rewind
|
||||
|
||||
# Loop over each entry and output file contents
|
||||
tar_extract.each do |entry|
|
||||
next if entry.directory?
|
||||
|
||||
# Oops! We don't check if the file is actually a symbolic link to a potentially sensitive file.
|
||||
puts entry.read
|
||||
end
|
||||
```
|
||||
|
||||
#### Go
|
||||
|
||||
```golang
|
||||
// printZipContents INSECURELY prints contents of files in a zip file.
|
||||
func printZipContents(src string) error {
|
||||
r, err := zip.OpenReader(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
// Loop over each entry and output file contents
|
||||
for _, f := range r.File {
|
||||
if f.FileInfo().IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
// Oops! We don't check if the file is actually a symbolic link to a potentially sensitive file.
|
||||
buf, err := ioutil.ReadAll(rc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(buf.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
#### Best practices
|
||||
|
||||
Always check the type of the archive entry before reading the contents and ignore entries that are not plain files. If you absolutely must support symbolic links, ensure that they only point to files inside the archive and nowhere else.
|
||||
|
||||
##### Ruby
|
||||
|
||||
```ruby
|
||||
# tar.gz extraction example with protection against symlink attacks.
|
||||
|
||||
begin
|
||||
tar_extract = Gem::Package::TarReader.new(Zlib::GzipReader.open("/tmp/uploaded.tar.gz"))
|
||||
rescue Errno::ENOENT
|
||||
STDERR.puts("archive file does not exist or is not readable")
|
||||
exit(false)
|
||||
end
|
||||
tar_extract.rewind
|
||||
|
||||
# Loop over each entry and output file contents
|
||||
tar_extract.each do |entry|
|
||||
next if entry.directory?
|
||||
|
||||
# By skipping symbolic links entirely, we are sure they can't cause any trouble!
|
||||
next if entry.symlink?
|
||||
|
||||
puts entry.read
|
||||
end
|
||||
```
|
||||
|
||||
##### Go
|
||||
|
||||
You are encouraged to use the secure archive utilities provided by [LabSec](https://gitlab.com/gitlab-com/gl-security/appsec/labsec) which will handle Zip Slip and symlink vulnerabilities for you. The LabSec utilities are also context aware which makes it possible to cancel or timeout extractions.
|
||||
|
||||
In case the LabSec utilities do not fit your needs, here is an example for extracting a zip file with protection against symlink attacks:
|
||||
|
||||
```golang
|
||||
// printZipContents prints contents of files in a zip file with protection against symlink attacks.
|
||||
func printZipContents(src string) error {
|
||||
r, err := zip.OpenReader(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
// Loop over each entry and output file contents
|
||||
for _, f := range r.File {
|
||||
if f.FileInfo().IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
// By skipping all irregular file types (including symbolic links), we are sure they can't cause any trouble!
|
||||
if !zf.Mode().IsRegular() {
|
||||
continue
|
||||
}
|
||||
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
buf, err := ioutil.ReadAll(rc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(buf.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
|
|
@ -11,25 +11,32 @@ GitLab SaaS is the GitLab software-as-a-service offering, which is available at
|
|||
You don't need to install anything to use GitLab SaaS, you only need to
|
||||
[sign up](https://gitlab.com/users/sign_up). When you sign up, you choose:
|
||||
|
||||
- [A license tier](https://about.gitlab.com/pricing/).
|
||||
- [A subscription](https://about.gitlab.com/pricing/).
|
||||
- [The number of seats you want](#how-seat-usage-is-determined).
|
||||
|
||||
All GitLab SaaS public projects, regardless of the subscription, get access to features in the **Ultimate** tier.
|
||||
The subscription determines which features are available for your private projects. Public projects automatically get **Ultimate** tier features.
|
||||
|
||||
Qualifying open source projects also get 50,000 CI/CD minutes and free access to the **Ultimate** tier
|
||||
through the [GitLab for Open Source program](https://about.gitlab.com/solutions/open-source/).
|
||||
|
||||
## Obtain a GitLab SaaS subscription
|
||||
|
||||
A GitLab SaaS subscription applies to a top-level group.
|
||||
Members of every subgroup and project in the group:
|
||||
|
||||
- Can use the features of the subscription.
|
||||
- Consume seats in the subscription.
|
||||
|
||||
To subscribe to GitLab SaaS:
|
||||
|
||||
1. View the [GitLab SaaS feature comparison](https://about.gitlab.com/pricing/gitlab-com/feature-comparison/)
|
||||
and decide which tier you want.
|
||||
1. Create a user account for yourself by using the
|
||||
[sign up page](https://gitlab.com/users/sign_up).
|
||||
1. Create a [group](../../user/group/index.md#create-a-group). You use the group to grant users access to several projects
|
||||
at once. A group is not required if you plan to have projects in a personal namespace instead.
|
||||
1. Create a [group](../../user/group/index.md#create-a-group). Your license tier applies to the top-level group, its subgroups, and projects.
|
||||
1. Create additional users and
|
||||
[add them to the group](../../user/group/index.md#add-users-to-a-group).
|
||||
[add them to the group](../../user/group/index.md#add-users-to-a-group). The users in this group, its subgroups, and projects can use
|
||||
the features of your license tier, and they consume a seat in your subscription.
|
||||
1. On the left sidebar, select **Billing** and choose a tier.
|
||||
1. Fill out the form to complete your purchase.
|
||||
|
||||
|
@ -62,10 +69,12 @@ The following information is displayed:
|
|||
email address.
|
||||
|
||||
A GitLab SaaS subscription uses a concurrent (_seat_) model. You pay for a
|
||||
subscription according to the maximum number of users enabled at one time. You can
|
||||
subscription according to the maximum number of users assigned to the top-level group or its children during the billing period. You can
|
||||
add and remove users during the subscription period, as long as the total users
|
||||
at any given time doesn't exceed the subscription count.
|
||||
|
||||
A top-level group can be [changed](../../user/group/index.md#change-a-groups-path) like any other group.
|
||||
|
||||
Every user is included in seat usage, with the following exceptions:
|
||||
|
||||
- Users who are pending approval.
|
||||
|
@ -77,6 +86,12 @@ Every user is included in seat usage, with the following exceptions:
|
|||
|
||||
Seat usage is reviewed [quarterly or annually](../quarterly_reconciliation.md).
|
||||
|
||||
If a user navigates to a different top-level group (one they have created themselves, for example)
|
||||
and that group does not have a paid subscription, they would not see any of the paid features.
|
||||
|
||||
It is also possible for users to belong to two different top-level groups with different subscriptions.
|
||||
In this case, they would see only the features available to that subscription.
|
||||
|
||||
### View seat usage
|
||||
|
||||
To view a list of seats being used:
|
||||
|
@ -124,7 +139,7 @@ and is not affected by the current search.
|
|||
A GitLab subscription is valid for a specific number of users.
|
||||
|
||||
If the number of billable users exceeds the number included in the subscription, known
|
||||
as the number of **seats owed**, you must pay for the excess number of users before renewal.
|
||||
as the number of **seats owed**, you must pay for the excess number of users.
|
||||
|
||||
For example, if you purchase a subscription for 10 users:
|
||||
|
||||
|
@ -138,9 +153,9 @@ Seats owed = 12 - 10 (Maximum users - users in subscription)
|
|||
|
||||
### Add users to your subscription
|
||||
|
||||
You can add users to your subscription at any time during the subscription period. The cost of
|
||||
additional users added during the subscription period is prorated from the date of purchase through
|
||||
the end of the subscription period.
|
||||
Your subscription cost is based on the maximum number of seats you use during the billing period.
|
||||
Even if you reach the number of seats in your subscription, you can continue to add users.
|
||||
GitLab [bills you for the overage](../quarterly_reconciliation.md).
|
||||
|
||||
To add users to a subscription:
|
||||
|
||||
|
|
|
@ -333,3 +333,14 @@ Planned removal milestone: 15.0 (2021-06-22)
|
|||
Tracing in GitLab is an integration with Jaeger, an open-source end-to-end distributed tracing system. GitLab users can navigate to their Jaeger instance to gain insight into the performance of a deployed application, tracking each function or microservice that handles a given request. Tracing in GitLab is deprecated in GitLab 14.7, and scheduled for removal in 15.0. To track work on a possible replacement, see the issue for [Opstrace integration with GitLab](https://gitlab.com/groups/gitlab-org/-/epics/6976).
|
||||
|
||||
Planned removal milestone: 15.0 (2022-05-22)
|
||||
|
||||
## 14.8
|
||||
|
||||
### Removal of `artifacts:report:cobertura` keyword
|
||||
|
||||
Currently, test coverage visualizations in GitLab only support Cobertura reports. Starting 15.0, the
|
||||
`artifacts:report:cobertura` keyword will be replaced by
|
||||
[`artifacts:reports:coverage_report`](https://gitlab.com/gitlab-org/gitlab/-/issues/344533). Cobertura will be the
|
||||
only supported report file in 15.0, but this is the first step towards GitLab supporting other report types.
|
||||
|
||||
Planned removal milestone: 15.0 (2022-06-22)
|
||||
|
|
|
@ -918,6 +918,8 @@ gemnasium-dependency_scanning:
|
|||
|
||||
## Warnings
|
||||
|
||||
We recommend that you use the most recent version of all containers, and the most recent supported version of all package managers and languages. Using previous versions carries an increased security risk because unsupported versions may no longer benefit from active security reporting and backporting of security fixes.
|
||||
|
||||
### Python projects
|
||||
|
||||
Extra care needs to be taken when using the [`PIP_EXTRA_INDEX_URL`](https://pipenv.pypa.io/en/latest/cli/#envvar-PIP_EXTRA_INDEX_URL)
|
||||
|
|
|
@ -187,7 +187,7 @@ describe('~/environments/components/new_environments_app.vue', () => {
|
|||
|
||||
expect(environmentAppMock).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({ scope: 'stopped' }),
|
||||
expect.objectContaining({ scope: 'stopped', page: 1 }),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
);
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
|
||||
let wrapper;
|
||||
|
||||
function createWrapper(propsData, mergeRequestWidgetGraphql) {
|
||||
function createWrapper(propsData, mergeRequestWidgetGraphql, rebaseWithoutCiUi) {
|
||||
wrapper = shallowMount(WidgetRebase, {
|
||||
propsData,
|
||||
data() {
|
||||
|
@ -24,7 +24,7 @@ function createWrapper(propsData, mergeRequestWidgetGraphql) {
|
|||
},
|
||||
};
|
||||
},
|
||||
provide: { glFeatures: { mergeRequestWidgetGraphql } },
|
||||
provide: { glFeatures: { mergeRequestWidgetGraphql, rebaseWithoutCiUi } },
|
||||
mocks: {
|
||||
$apollo: {
|
||||
queries: {
|
||||
|
@ -38,7 +38,8 @@ function createWrapper(propsData, mergeRequestWidgetGraphql) {
|
|||
describe('Merge request widget rebase component', () => {
|
||||
const findRebaseMessage = () => wrapper.find('[data-testid="rebase-message"]');
|
||||
const findRebaseMessageText = () => findRebaseMessage().text();
|
||||
const findRebaseButton = () => wrapper.find(ActionsButton);
|
||||
const findRebaseButtonActions = () => wrapper.find(ActionsButton);
|
||||
const findStandardRebaseButton = () => wrapper.find('[data-testid="standard-rebase-button"]');
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
|
@ -65,7 +66,7 @@ describe('Merge request widget rebase component', () => {
|
|||
const rebaseMock = jest.fn().mockResolvedValue();
|
||||
const pollMock = jest.fn().mockResolvedValue({});
|
||||
|
||||
beforeEach(() => {
|
||||
it('renders the warning message', () => {
|
||||
createWrapper(
|
||||
{
|
||||
mr: {
|
||||
|
@ -79,9 +80,7 @@ describe('Merge request widget rebase component', () => {
|
|||
},
|
||||
mergeRequestWidgetGraphql,
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the warning message', () => {
|
||||
const text = findRebaseMessageText();
|
||||
|
||||
expect(text).toContain('Merge blocked');
|
||||
|
@ -91,6 +90,20 @@ describe('Merge request widget rebase component', () => {
|
|||
});
|
||||
|
||||
it('renders an error message when rebasing has failed', async () => {
|
||||
createWrapper(
|
||||
{
|
||||
mr: {
|
||||
rebaseInProgress: false,
|
||||
canPushToSourceBranch: true,
|
||||
},
|
||||
service: {
|
||||
rebase: rebaseMock,
|
||||
poll: pollMock,
|
||||
},
|
||||
},
|
||||
mergeRequestWidgetGraphql,
|
||||
);
|
||||
|
||||
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
wrapper.setData({ rebasingError: 'Something went wrong!' });
|
||||
|
@ -99,13 +112,31 @@ describe('Merge request widget rebase component', () => {
|
|||
expect(findRebaseMessageText()).toContain('Something went wrong!');
|
||||
});
|
||||
|
||||
describe('"Rebase" button', () => {
|
||||
it('is rendered', () => {
|
||||
expect(findRebaseButton().exists()).toBe(true);
|
||||
describe('Rebase button with flag rebaseWithoutCiUi', () => {
|
||||
beforeEach(() => {
|
||||
createWrapper(
|
||||
{
|
||||
mr: {
|
||||
rebaseInProgress: false,
|
||||
canPushToSourceBranch: true,
|
||||
},
|
||||
service: {
|
||||
rebase: rebaseMock,
|
||||
poll: pollMock,
|
||||
},
|
||||
},
|
||||
mergeRequestWidgetGraphql,
|
||||
{ rebaseWithoutCiUi: true },
|
||||
);
|
||||
});
|
||||
|
||||
it('rebase button with actions is rendered', () => {
|
||||
expect(findRebaseButtonActions().exists()).toBe(true);
|
||||
expect(findStandardRebaseButton().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('has rebase and rebase without CI actions', () => {
|
||||
const actionNames = findRebaseButton()
|
||||
const actionNames = findRebaseButtonActions()
|
||||
.props('actions')
|
||||
.map((action) => action.key);
|
||||
|
||||
|
@ -113,13 +144,13 @@ describe('Merge request widget rebase component', () => {
|
|||
});
|
||||
|
||||
it('defaults to rebase action', () => {
|
||||
expect(findRebaseButton().props('selectedKey')).toStrictEqual(REBASE_BUTTON_KEY);
|
||||
expect(findRebaseButtonActions().props('selectedKey')).toStrictEqual(REBASE_BUTTON_KEY);
|
||||
});
|
||||
|
||||
it('starts the rebase when clicking', async () => {
|
||||
// ActionButtons use the actions props instead of emitting
|
||||
// a click event, therefore simulating the behavior here:
|
||||
findRebaseButton()
|
||||
findRebaseButtonActions()
|
||||
.props('actions')
|
||||
.find((x) => x.key === REBASE_BUTTON_KEY)
|
||||
.handle();
|
||||
|
@ -132,7 +163,7 @@ describe('Merge request widget rebase component', () => {
|
|||
it('starts the CI-skipping rebase when clicking on "Rebase without CI"', async () => {
|
||||
// ActionButtons use the actions props instead of emitting
|
||||
// a click event, therefore simulating the behavior here:
|
||||
findRebaseButton()
|
||||
findRebaseButtonActions()
|
||||
.props('actions')
|
||||
.find((x) => x.key === REBASE_WITHOUT_CI_BUTTON_KEY)
|
||||
.handle();
|
||||
|
@ -142,12 +173,91 @@ describe('Merge request widget rebase component', () => {
|
|||
expect(rebaseMock).toHaveBeenCalledWith({ skipCi: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Rebase button with rebaseWithoutCiUI flag disabled', () => {
|
||||
beforeEach(() => {
|
||||
createWrapper(
|
||||
{
|
||||
mr: {
|
||||
rebaseInProgress: false,
|
||||
canPushToSourceBranch: true,
|
||||
},
|
||||
service: {
|
||||
rebase: rebaseMock,
|
||||
poll: pollMock,
|
||||
},
|
||||
},
|
||||
mergeRequestWidgetGraphql,
|
||||
);
|
||||
});
|
||||
|
||||
it('standard rebase button is rendered', () => {
|
||||
expect(findStandardRebaseButton().exists()).toBe(true);
|
||||
expect(findRebaseButtonActions().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('calls rebase method with skip_ci false', () => {
|
||||
findStandardRebaseButton().vm.$emit('click');
|
||||
|
||||
expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('without permissions', () => {
|
||||
const exampleTargetBranch = 'fake-branch-to-test-with';
|
||||
|
||||
beforeEach(() => {
|
||||
describe('UI text', () => {
|
||||
beforeEach(() => {
|
||||
createWrapper(
|
||||
{
|
||||
mr: {
|
||||
rebaseInProgress: false,
|
||||
canPushToSourceBranch: false,
|
||||
targetBranch: exampleTargetBranch,
|
||||
},
|
||||
service: {},
|
||||
},
|
||||
mergeRequestWidgetGraphql,
|
||||
);
|
||||
});
|
||||
|
||||
it('renders a message explaining user does not have permissions', () => {
|
||||
const text = findRebaseMessageText();
|
||||
|
||||
expect(text).toContain(
|
||||
'Merge blocked: the source branch must be rebased onto the target branch.',
|
||||
);
|
||||
expect(text).toContain('the source branch must be rebased');
|
||||
});
|
||||
|
||||
it('renders the correct target branch name', () => {
|
||||
const elem = findRebaseMessage();
|
||||
|
||||
expect(elem.text()).toContain(
|
||||
'Merge blocked: the source branch must be rebased onto the target branch.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not render the rebase actions button with rebaseWithoutCiUI flag enabled', () => {
|
||||
createWrapper(
|
||||
{
|
||||
mr: {
|
||||
rebaseInProgress: false,
|
||||
canPushToSourceBranch: false,
|
||||
targetBranch: exampleTargetBranch,
|
||||
},
|
||||
service: {},
|
||||
},
|
||||
mergeRequestWidgetGraphql,
|
||||
{ rebaseWithoutCiUi: true },
|
||||
);
|
||||
|
||||
expect(findRebaseButtonActions().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('does not render the standard rebase button with rebaseWithoutCiUI flag disabled', () => {
|
||||
createWrapper(
|
||||
{
|
||||
mr: {
|
||||
|
@ -159,27 +269,8 @@ describe('Merge request widget rebase component', () => {
|
|||
},
|
||||
mergeRequestWidgetGraphql,
|
||||
);
|
||||
});
|
||||
|
||||
it('renders a message explaining user does not have permissions', () => {
|
||||
const text = findRebaseMessageText();
|
||||
|
||||
expect(text).toContain(
|
||||
'Merge blocked: the source branch must be rebased onto the target branch.',
|
||||
);
|
||||
expect(text).toContain('the source branch must be rebased');
|
||||
});
|
||||
|
||||
it('renders the correct target branch name', () => {
|
||||
const elem = findRebaseMessage();
|
||||
|
||||
expect(elem.text()).toContain(
|
||||
`Merge blocked: the source branch must be rebased onto the target branch.`,
|
||||
);
|
||||
});
|
||||
|
||||
it('does not render the "Rebase" button', () => {
|
||||
expect(findRebaseButton().exists()).toBe(false);
|
||||
expect(findStandardRebaseButton().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ RSpec.describe GitlabSchema.types['Group'] do
|
|||
dependency_proxy_blobs dependency_proxy_image_count
|
||||
dependency_proxy_blob_count dependency_proxy_total_size
|
||||
dependency_proxy_image_prefix dependency_proxy_image_ttl_policy
|
||||
shared_runners_setting timelogs organizations contacts
|
||||
shared_runners_setting timelogs organizations contacts work_item_types
|
||||
]
|
||||
|
||||
expect(described_class).to include_graphql_fields(*expected_fields)
|
||||
|
|
|
@ -34,7 +34,7 @@ RSpec.describe GitlabSchema.types['Project'] do
|
|||
container_repositories container_repositories_count
|
||||
pipeline_analytics squash_read_only sast_ci_configuration
|
||||
cluster_agent cluster_agents agent_configurations
|
||||
ci_template timelogs merge_commit_template squash_commit_template
|
||||
ci_template timelogs merge_commit_template squash_commit_template work_item_types
|
||||
]
|
||||
|
||||
expect(described_class).to include_graphql_fields(*expected_fields)
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'getting a list of work item types for a project' do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:developer) { create(:user) }
|
||||
let_it_be(:project) { create(:project) }
|
||||
|
||||
before_all do
|
||||
project.add_developer(developer)
|
||||
end
|
||||
|
||||
let(:current_user) { developer }
|
||||
|
||||
let(:fields) do
|
||||
<<~GRAPHQL
|
||||
workItemTypes{
|
||||
nodes { id name iconName }
|
||||
}
|
||||
GRAPHQL
|
||||
end
|
||||
|
||||
let(:query) do
|
||||
graphql_query_for(
|
||||
'project',
|
||||
{ 'fullPath' => project.full_path },
|
||||
fields
|
||||
)
|
||||
end
|
||||
|
||||
context 'when user has access to the project' do
|
||||
before do
|
||||
post_graphql(query, current_user: current_user)
|
||||
end
|
||||
|
||||
it_behaves_like 'a working graphql query'
|
||||
|
||||
it 'returns all default work item types' do
|
||||
expect(graphql_data.dig('project', 'workItemTypes', 'nodes')).to match_array(
|
||||
WorkItems::Type.default.map do |type|
|
||||
hash_including('id' => type.to_global_id.to_s, 'name' => type.name, 'iconName' => type.icon_name)
|
||||
end
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "when user doesn't have access to the project" do
|
||||
let(:current_user) { create(:user) }
|
||||
|
||||
before do
|
||||
post_graphql(query, current_user: current_user)
|
||||
end
|
||||
|
||||
it 'does not return the project' do
|
||||
expect(graphql_data).to eq('project' => nil)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the work_items feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(work_items: false)
|
||||
post_graphql(query, current_user: current_user)
|
||||
end
|
||||
|
||||
it 'makes the workItemTypes field unavailable' do
|
||||
expect(graphql_errors).to contain_exactly(hash_including("message" => "Field 'workItemTypes' doesn't exist on type 'Project'"))
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue