Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
e80e0dd64f
commit
dc003cd08b
|
@ -1,5 +1,12 @@
|
|||
Please view this file on the master branch, on stable branches it's out of date.
|
||||
|
||||
## 12.9.1 (2020-03-26)
|
||||
|
||||
### Security (1 change)
|
||||
|
||||
- Add NPM package versions SemVer validation.
|
||||
|
||||
|
||||
## 12.9.0 (2020-03-22)
|
||||
|
||||
### Removed (1 change)
|
||||
|
|
26
CHANGELOG.md
26
CHANGELOG.md
|
@ -2,6 +2,32 @@
|
|||
documentation](doc/development/changelog.md) for instructions on adding your own
|
||||
entry.
|
||||
|
||||
## 12.9.1 (2020-03-26)
|
||||
|
||||
### Security (16 changes)
|
||||
|
||||
- Add permission check for pipeline status of MR.
|
||||
- Ignore empty remote_id params from Workhorse accelerated uploads.
|
||||
- External user can not create personal snippet through API.
|
||||
- Prevent malicious entry for group name.
|
||||
- Restrict mirroring changes to admins only when mirroring is disabled.
|
||||
- Reject all container registry requests from blocked users.
|
||||
- Deny localhost requests on fogbugz importer.
|
||||
- Redact notes in moved confidential issues.
|
||||
- Fix UploadRewriter Path Traversal vulnerability.
|
||||
- Block hotlinking to repository archives.
|
||||
- Restrict access to project pipeline metrics reports.
|
||||
- vulnerability_feedback records should be restricted to a dev role and above.
|
||||
- Exclude Carrierwave remote URL methods from import.
|
||||
- Update Nokogiri to fix CVE-2020-7595.
|
||||
- Prevent updating trigger by other maintainers.
|
||||
- Fix XSS vulnerability in `admin/email` "Recipient Group" dropdown.
|
||||
|
||||
### Fixed (1 change)
|
||||
|
||||
- Fix updating the authorized_keys file. !27798
|
||||
|
||||
|
||||
## 12.9.0 (2020-03-22)
|
||||
|
||||
### Security (1 change)
|
||||
|
|
|
@ -1 +1 @@
|
|||
8.27.0
|
||||
8.28.0
|
||||
|
|
|
@ -652,7 +652,7 @@ GEM
|
|||
netrc (0.11.0)
|
||||
nio4r (2.5.2)
|
||||
no_proxy_fix (0.1.2)
|
||||
nokogiri (1.10.7)
|
||||
nokogiri (1.10.8)
|
||||
mini_portile2 (~> 2.4.0)
|
||||
nokogumbo (1.5.0)
|
||||
nokogiri
|
||||
|
|
|
@ -107,8 +107,12 @@ export default {
|
|||
isProjectCluster() {
|
||||
return this.type === CLUSTER_TYPE.PROJECT;
|
||||
},
|
||||
managedAppsLocalTillerEnabled() {
|
||||
return Boolean(gon.features?.managedAppsLocalTiller);
|
||||
},
|
||||
helmInstalled() {
|
||||
return (
|
||||
this.managedAppsLocalTillerEnabled ||
|
||||
this.applications.helm.status === APPLICATION_STATUS.INSTALLED ||
|
||||
this.applications.helm.status === APPLICATION_STATUS.UPDATED
|
||||
);
|
||||
|
@ -270,6 +274,7 @@ Crossplane runs inside your Kubernetes cluster and supports secure connectivity
|
|||
|
||||
<div class="cluster-application-list prepend-top-10">
|
||||
<application-row
|
||||
v-if="!managedAppsLocalTillerEnabled"
|
||||
id="helm"
|
||||
:logo-url="helmLogo"
|
||||
:title="applications.helm.title"
|
||||
|
|
|
@ -45,8 +45,19 @@ export const updateExistingFrequentItem = (frequentItem, item) => {
|
|||
};
|
||||
};
|
||||
|
||||
export const sanitizeItem = item => ({
|
||||
...item,
|
||||
name: sanitize(item.name.toString(), { allowedTags: [] }),
|
||||
namespace: sanitize(item.namespace.toString(), { allowedTags: [] }),
|
||||
});
|
||||
export const sanitizeItem = item => {
|
||||
// Only sanitize if the key exists on the item
|
||||
const maybeSanitize = key => {
|
||||
if (!Object.prototype.hasOwnProperty.call(item, key)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return { [key]: sanitize(item[key].toString(), { allowedTags: [] }) };
|
||||
};
|
||||
|
||||
return {
|
||||
...item,
|
||||
...maybeSanitize('name'),
|
||||
...maybeSanitize('namespace'),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,19 +1 @@
|
|||
import initNotes from '~/init_notes';
|
||||
import ZenMode from '~/zen_mode';
|
||||
import LineHighlighter from '~/line_highlighter';
|
||||
import BlobViewer from '~/blob/viewer';
|
||||
import snippetEmbed from '~/snippet/snippet_embed';
|
||||
import { SnippetShowInit } from '~/snippets';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (!gon.features.snippetsVue) {
|
||||
new LineHighlighter(); // eslint-disable-line no-new
|
||||
new BlobViewer(); // eslint-disable-line no-new
|
||||
initNotes();
|
||||
new ZenMode(); // eslint-disable-line no-new
|
||||
snippetEmbed();
|
||||
} else {
|
||||
SnippetShowInit();
|
||||
initNotes();
|
||||
}
|
||||
});
|
||||
import '~/snippet/snippet_show';
|
||||
|
|
|
@ -1,19 +1 @@
|
|||
import LineHighlighter from '~/line_highlighter';
|
||||
import BlobViewer from '~/blob/viewer';
|
||||
import ZenMode from '~/zen_mode';
|
||||
import initNotes from '~/init_notes';
|
||||
import snippetEmbed from '~/snippet/snippet_embed';
|
||||
import { SnippetShowInit } from '~/snippets';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (!gon.features.snippetsVue) {
|
||||
new LineHighlighter(); // eslint-disable-line no-new
|
||||
new BlobViewer(); // eslint-disable-line no-new
|
||||
initNotes();
|
||||
new ZenMode(); // eslint-disable-line no-new
|
||||
snippetEmbed();
|
||||
} else {
|
||||
SnippetShowInit();
|
||||
initNotes();
|
||||
}
|
||||
});
|
||||
import '~/snippet/snippet_show';
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
import LineHighlighter from '~/line_highlighter';
|
||||
import BlobViewer from '~/blob/viewer';
|
||||
import ZenMode from '~/zen_mode';
|
||||
import initNotes from '~/init_notes';
|
||||
import snippetEmbed from '~/snippet/snippet_embed';
|
||||
import { SnippetShowInit } from '~/snippets';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (!gon.features.snippetsVue) {
|
||||
new LineHighlighter(); // eslint-disable-line no-new
|
||||
new BlobViewer(); // eslint-disable-line no-new
|
||||
initNotes();
|
||||
new ZenMode(); // eslint-disable-line no-new
|
||||
snippetEmbed();
|
||||
} else {
|
||||
SnippetShowInit();
|
||||
initNotes();
|
||||
}
|
||||
});
|
|
@ -6,6 +6,10 @@ class Clusters::BaseController < ApplicationController
|
|||
skip_before_action :authenticate_user!
|
||||
before_action :authorize_read_cluster!
|
||||
|
||||
before_action do
|
||||
push_frontend_feature_flag(:managed_apps_local_tiller)
|
||||
end
|
||||
|
||||
helper_method :clusterable
|
||||
|
||||
private
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module HotlinkInterceptor
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def intercept_hotlinking!
|
||||
return render_406 if Gitlab::HotlinkingDetector.intercept_hotlinking?(request)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def render_406
|
||||
head :not_acceptable
|
||||
end
|
||||
end
|
|
@ -3,6 +3,7 @@
|
|||
class Import::FogbugzController < Import::BaseController
|
||||
before_action :verify_fogbugz_import_enabled
|
||||
before_action :user_map, only: [:new_user_map, :create_user_map]
|
||||
before_action :verify_blocked_uri, only: :callback
|
||||
|
||||
rescue_from Fogbugz::AuthenticationException, with: :fogbugz_unauthorized
|
||||
|
||||
|
@ -106,4 +107,21 @@ class Import::FogbugzController < Import::BaseController
|
|||
def verify_fogbugz_import_enabled
|
||||
render_404 unless fogbugz_import_enabled?
|
||||
end
|
||||
|
||||
def verify_blocked_uri
|
||||
Gitlab::UrlBlocker.validate!(
|
||||
params[:uri],
|
||||
{
|
||||
allow_localhost: allow_local_requests?,
|
||||
allow_local_network: allow_local_requests?,
|
||||
schemes: %w(http https)
|
||||
}
|
||||
)
|
||||
rescue Gitlab::UrlBlocker::BlockedUrlError => e
|
||||
redirect_to new_import_fogbugz_url, alert: _('Specified URL cannot be used: "%{reason}"') % { reason: e.message }
|
||||
end
|
||||
|
||||
def allow_local_requests?
|
||||
Gitlab::CurrentSettings.allow_local_requests_from_web_hooks_and_services?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -67,7 +67,7 @@ class Projects::MirrorsController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def check_mirror_available!
|
||||
Gitlab::CurrentSettings.current_application_settings.mirror_available || current_user&.admin?
|
||||
render_404 unless can?(current_user, :admin_remote_mirror, project)
|
||||
end
|
||||
|
||||
def mirror_params_attributes
|
||||
|
|
|
@ -4,12 +4,14 @@ class Projects::RepositoriesController < Projects::ApplicationController
|
|||
include ExtractsPath
|
||||
include StaticObjectExternalStorage
|
||||
include Gitlab::RateLimitHelpers
|
||||
include HotlinkInterceptor
|
||||
|
||||
prepend_before_action(only: [:archive]) { authenticate_sessionless_user!(:archive) }
|
||||
|
||||
# Authorize
|
||||
before_action :require_non_empty_project, except: :create
|
||||
before_action :archive_rate_limit!, only: :archive
|
||||
before_action :intercept_hotlinking!, only: :archive
|
||||
before_action :assign_archive_vars, only: :archive
|
||||
before_action :assign_append_sha, only: :archive
|
||||
before_action :authorize_download_code!
|
||||
|
|
|
@ -70,6 +70,9 @@ class Group < Namespace
|
|||
validates :variables, variable_duplicates: true
|
||||
|
||||
validates :two_factor_grace_period, presence: true, numericality: { greater_than_or_equal_to: 0 }
|
||||
validates :name,
|
||||
format: { with: Gitlab::Regex.group_name_regex,
|
||||
message: Gitlab::Regex.group_name_regex_message }
|
||||
|
||||
add_authentication_token_field :runners_token, encrypted: -> { Feature.enabled?(:groups_tokens_optional_encryption, default_enabled: true) ? :optional : :required }
|
||||
|
||||
|
|
|
@ -326,10 +326,8 @@ class Issue < ApplicationRecord
|
|||
true
|
||||
elsif project.owner == user
|
||||
true
|
||||
elsif confidential?
|
||||
author == user ||
|
||||
assignees.include?(user) ||
|
||||
project.team.member?(user, Gitlab::Access::REPORTER)
|
||||
elsif confidential? && !assignee_or_author?(user)
|
||||
project.team.member?(user, Gitlab::Access::REPORTER)
|
||||
else
|
||||
project.public? ||
|
||||
project.internal? && !user.external? ||
|
||||
|
|
|
@ -53,7 +53,7 @@ class MergeRequestPollWidgetEntity < Grape::Entity
|
|||
|
||||
# CI related
|
||||
expose :has_ci?, as: :has_ci
|
||||
expose :ci_status do |merge_request|
|
||||
expose :ci_status, if: -> (mr, _) { presenter(mr).can_read_pipeline? } do |merge_request|
|
||||
presenter(merge_request).ci_status
|
||||
end
|
||||
|
||||
|
|
|
@ -318,7 +318,7 @@ module ObjectStorage
|
|||
def cache!(new_file = sanitized_file)
|
||||
# We intercept ::UploadedFile which might be stored on remote storage
|
||||
# We use that for "accelerated" uploads, where we store result on remote storage
|
||||
if new_file.is_a?(::UploadedFile) && new_file.remote_id
|
||||
if new_file.is_a?(::UploadedFile) && new_file.remote_id.present?
|
||||
return cache_remote_file!(new_file.remote_id, new_file.original_filename)
|
||||
end
|
||||
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
- expanded = expanded_by_default?
|
||||
- protocols = Gitlab::UrlSanitizer::ALLOWED_SCHEMES.join('|')
|
||||
- mirror_settings_enabled = can?(current_user, :admin_remote_mirror, @project)
|
||||
- mirror_settings_class = "#{'expanded' if expanded} #{'js-mirror-settings' if mirror_settings_enabled}".strip
|
||||
|
||||
%section.settings.project-mirror-settings.js-mirror-settings.no-animate#js-push-remote-settings{ class: ('expanded' if expanded), data: { qa_selector: 'mirroring_repositories_settings_section' } }
|
||||
%section.settings.project-mirror-settings.no-animate#js-push-remote-settings{ class: mirror_settings_class, data: { qa_selector: 'mirroring_repositories_settings_section' } }
|
||||
.settings-header
|
||||
%h4= _('Mirroring repositories')
|
||||
%button.btn.js-settings-toggle
|
||||
|
@ -11,26 +13,32 @@
|
|||
= link_to _('Read more'), help_page_path('workflow/repository_mirroring'), target: '_blank'
|
||||
|
||||
.settings-content
|
||||
= form_for @project, url: project_mirror_path(@project), html: { class: 'gl-show-field-errors js-mirror-form', autocomplete: 'new-password', data: mirrors_form_data_attributes } do |f|
|
||||
.panel.panel-default
|
||||
.panel-body
|
||||
%div= form_errors(@project)
|
||||
- if mirror_settings_enabled
|
||||
= form_for @project, url: project_mirror_path(@project), html: { class: 'gl-show-field-errors js-mirror-form', autocomplete: 'new-password', data: mirrors_form_data_attributes } do |f|
|
||||
.panel.panel-default
|
||||
.panel-body
|
||||
%div= form_errors(@project)
|
||||
|
||||
.form-group.has-feedback
|
||||
= label_tag :url, _('Git repository URL'), class: 'label-light'
|
||||
= text_field_tag :url, nil, class: 'form-control js-mirror-url js-repo-url qa-mirror-repository-url-input', placeholder: _('Input your repository URL'), required: true, pattern: "(#{protocols}):\/\/.+", autocomplete: 'new-password'
|
||||
.form-group.has-feedback
|
||||
= label_tag :url, _('Git repository URL'), class: 'label-light'
|
||||
= text_field_tag :url, nil, class: 'form-control js-mirror-url js-repo-url qa-mirror-repository-url-input', placeholder: _('Input your repository URL'), required: true, pattern: "(#{protocols}):\/\/.+", autocomplete: 'new-password'
|
||||
|
||||
= render 'projects/mirrors/instructions'
|
||||
= render 'projects/mirrors/instructions'
|
||||
|
||||
= render 'projects/mirrors/mirror_repos_form', f: f
|
||||
= render 'projects/mirrors/mirror_repos_form', f: f
|
||||
|
||||
.form-check.append-bottom-10
|
||||
= check_box_tag :only_protected_branches, '1', false, class: 'js-mirror-protected form-check-input'
|
||||
= label_tag :only_protected_branches, _('Only mirror protected branches'), class: 'form-check-label'
|
||||
= link_to icon('question-circle'), help_page_path('user/project/protected_branches'), target: '_blank'
|
||||
.form-check.append-bottom-10
|
||||
= check_box_tag :only_protected_branches, '1', false, class: 'js-mirror-protected form-check-input'
|
||||
= label_tag :only_protected_branches, _('Only mirror protected branches'), class: 'form-check-label'
|
||||
= link_to icon('question-circle'), help_page_path('user/project/protected_branches'), target: '_blank'
|
||||
|
||||
.panel-footer
|
||||
= f.submit _('Mirror repository'), class: 'btn btn-success js-mirror-submit qa-mirror-repository-button', name: :update_remote_mirror
|
||||
.panel-footer
|
||||
= f.submit _('Mirror repository'), class: 'btn btn-success js-mirror-submit qa-mirror-repository-button', name: :update_remote_mirror
|
||||
- else
|
||||
.gl-alert.gl-alert-info{ role: 'alert' }
|
||||
= sprite_icon('information-o', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
|
||||
.gl-alert-body
|
||||
= _('Mirror settings are only available to GitLab administrators.')
|
||||
|
||||
.panel.panel-default
|
||||
.table-responsive
|
||||
|
@ -61,8 +69,10 @@
|
|||
- if mirror.last_error.present?
|
||||
.badge.mirror-error-badge{ data: { toggle: 'tooltip', html: 'true', qa_selector: 'mirror_error_badge' }, title: html_escape(mirror.last_error.try(:strip)) }= _('Error')
|
||||
%td
|
||||
.btn-group.mirror-actions-group.pull-right{ role: 'group' }
|
||||
- if mirror.ssh_key_auth?
|
||||
= clipboard_button(text: mirror.ssh_public_key, class: 'btn btn-default', title: _('Copy SSH public key'), qa_selector: 'copy_public_key_button')
|
||||
= render 'shared/remote_mirror_update_button', remote_mirror: mirror
|
||||
- if mirror_settings_enabled
|
||||
.btn-group.mirror-actions-group.pull-right{ role: 'group' }
|
||||
- if mirror.ssh_key_auth?
|
||||
= clipboard_button(text: mirror.ssh_public_key, class: 'btn btn-default', title: _('Copy SSH public key'), qa_selector: 'copy_public_key_button')
|
||||
= render 'shared/remote_mirror_update_button', remote_mirror: mirror
|
||||
%button.js-delete-mirror.qa-delete-mirror.rspec-delete-mirror.btn.btn-danger{ type: 'button', data: { mirror_id: mirror.id, toggle: 'tooltip', container: 'body' }, title: _('Remove') }= icon('trash-o')
|
||||
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
title: Fix updating the authorized_keys file
|
||||
merge_request: 27798
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix optional params for deploy token API
|
||||
merge_request: 27961
|
||||
author: Nejc Habjan
|
||||
type: fixed
|
|
@ -1,5 +1,8 @@
|
|||
# The `table_exists?` check is needed because during our migration rollback testing,
|
||||
# `Shard.connected?` could be cached and return true even though the table doesn't exist
|
||||
if Shard.connected? && Shard.table_exists? && !Gitlab::Database.read_only?
|
||||
Shard.populate!
|
||||
end
|
||||
return unless Shard.connected?
|
||||
return unless Shard.table_exists?
|
||||
return unless Shard.connection.index_exists?(:shards, :name, unique: true)
|
||||
return if Gitlab::Database.read_only?
|
||||
|
||||
Shard.populate!
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
Binary file not shown.
After Width: | Height: | Size: 37 KiB |
|
@ -18,6 +18,8 @@ have its own space to store its Docker images.
|
|||
|
||||
You can read more about Docker Registry at <https://docs.docker.com/registry/introduction/>.
|
||||
|
||||
![Container Registry repositories](img/container_registry_repositories_v12_10.png)
|
||||
|
||||
## Enable the Container Registry for your project
|
||||
|
||||
CAUTION: **Warning:**
|
||||
|
@ -329,10 +331,124 @@ If you forget to set the service alias, the `docker:19.03.1` image won't find th
|
|||
error during connect: Get http://docker:2376/v1.39/info: dial tcp: lookup docker on 192.168.0.1:53: no such host
|
||||
```
|
||||
|
||||
## Delete images
|
||||
|
||||
You can delete images from your Container Registry in multiple ways.
|
||||
|
||||
CAUTION: **Warning:**
|
||||
Deleting images is a destructive action and can't be undone. To restore
|
||||
a deleted image, you must rebuild and re-upload it.
|
||||
|
||||
NOTE: **Note:**
|
||||
Administrators should review how to
|
||||
[garbage collect](../../../administration/packages/container_registry.md#container-registry-garbage-collection)
|
||||
the deleted images.
|
||||
|
||||
### Delete images from within GitLab
|
||||
|
||||
To delete images from within GitLab:
|
||||
|
||||
1. Navigate to your project's or group's **{package}** **Packages > Container Registry**.
|
||||
1. From the **Container Registry** page, you can select what you want to delete,
|
||||
by either:
|
||||
|
||||
- Deleting the entire repository, and all the tags it contains, by clicking
|
||||
the red **{remove}** **Trash** icon.
|
||||
- Navigating to the repository, and deleting tags individually or in bulk
|
||||
by clicking the red **{remove}** **Trash** icon next to the tag you want
|
||||
to delete.
|
||||
|
||||
1. In the dialog box, click **Remove tag**.
|
||||
|
||||
![Container Registry tags](img/container_registry_tags_v12_10.png)
|
||||
|
||||
### Delete images using the API
|
||||
|
||||
If you want to automate the process of deleting images, GitLab provides an API. For more
|
||||
information, see the following endpoints:
|
||||
|
||||
- [Delete a Registry repository](../../../api/container_registry.md#delete-registry-repository)
|
||||
- [Delete an individual Registry repository tag](../../../api/container_registry.md#delete-a-registry-repository-tag)
|
||||
- [Delete Registry repository tags in bulk](../../../api/container_registry.md#delete-registry-repository-tags-in-bulk)
|
||||
|
||||
### Delete images using GitLab CI/CD
|
||||
|
||||
CAUTION: **Warning:**
|
||||
GitLab CI/CD doesn't provide a built-in way to remove your images, but this example
|
||||
uses a third-party tool called [reg](https://github.com/genuinetools/reg)
|
||||
that talks to the GitLab Registry API. You are responsible for your own actions.
|
||||
For assistance with this tool, see
|
||||
[the issue queue for reg](https://github.com/genuinetools/reg/issues).
|
||||
|
||||
The following example defines two stages: `build`, and `clean`. The
|
||||
`build_image` job builds the Docker image for the branch, and the
|
||||
`delete_image` job deletes it. The `reg` executable is downloaded and used to
|
||||
remove the image matching the `$CI_PROJECT_PATH:$CI_COMMIT_REF_SLUG`
|
||||
[environment variable](../../../ci/variables/predefined_variables.md).
|
||||
|
||||
To use this example, change the `IMAGE_TAG` variable to match your needs:
|
||||
|
||||
```yaml
|
||||
stages:
|
||||
- build
|
||||
- clean
|
||||
|
||||
build_image:
|
||||
image: docker:19.03.1
|
||||
stage: build
|
||||
services:
|
||||
- docker:19.03.1-dind
|
||||
variables:
|
||||
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
|
||||
script:
|
||||
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
||||
- docker build -t $IMAGE_TAG .
|
||||
- docker push $IMAGE_TAG
|
||||
only:
|
||||
- branches
|
||||
except:
|
||||
- master
|
||||
|
||||
delete_image:
|
||||
image: docker:19.03.1
|
||||
stage: clean
|
||||
services:
|
||||
- docker:19.03.1-dind
|
||||
variables:
|
||||
IMAGE_TAG: $CI_PROJECT_PATH:$CI_COMMIT_REF_SLUG
|
||||
REG_SHA256: ade837fc5224acd8c34732bf54a94f579b47851cc6a7fd5899a98386b782e228
|
||||
REG_VERSION: 0.16.1
|
||||
before_script:
|
||||
- apk add --no-cache curl
|
||||
- curl --fail --show-error --location "https://github.com/genuinetools/reg/releases/download/v$REG_VERSION/reg-linux-amd64" --output /usr/local/bin/reg
|
||||
- echo "$REG_SHA256 /usr/local/bin/reg" | sha256sum -c -
|
||||
- chmod a+x /usr/local/bin/reg
|
||||
script:
|
||||
- /usr/local/bin/reg rm -d --auth-url $CI_REGISTRY -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $IMAGE_TAG
|
||||
only:
|
||||
- branches
|
||||
except:
|
||||
- master
|
||||
```
|
||||
|
||||
TIP: **Tip:**
|
||||
You can download the latest `reg` release from
|
||||
[the releases page](https://github.com/genuinetools/reg/releases), then update
|
||||
the code example by changing the `REG_SHA256` and `REG_VERSION` variables
|
||||
defined in the `delete_image` job.
|
||||
|
||||
### Delete images using an expiration policy
|
||||
|
||||
You can create a per-project [expiration policy](#expiration-policy) to ensure
|
||||
older tags and images are regularly removed from the Container Registry.
|
||||
|
||||
## Expiration policy
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/15398) in GitLab 12.8.
|
||||
|
||||
NOTE: **Note:**
|
||||
Expiration policies are only available for projects created in GitLab 12.8 and later.
|
||||
|
||||
It is possible to create a per-project expiration policy, so that you can make sure that
|
||||
older tags and images are regularly removed from the Container Registry.
|
||||
|
||||
|
@ -348,6 +464,20 @@ then goes through a process of excluding tags from it until only the ones to be
|
|||
1. Excludes from the list the tags older than the `older_than` value (Expiration interval).
|
||||
1. Finally, the remaining tags in the list are deleted from the Container Registry.
|
||||
|
||||
### Managing project expiration policy through the UI
|
||||
|
||||
To manage project expiration policy, navigate to **{settings}** **Settings > CI/CD > Container Registry tag expiration policy**.
|
||||
|
||||
![Expiration Policy App](img/expiration-policy-app.png)
|
||||
|
||||
The UI allows you to configure the following:
|
||||
|
||||
- **Expiration policy:** enable or disable the expiration policy.
|
||||
- **Expiration interval:** how long tags are exempt from being deleted.
|
||||
- **Expiration schedule:** how often the cron job checking the tags should run.
|
||||
- **Expiration latest:** how many tags to _always_ keep for each image.
|
||||
- **Docker tags with names matching this regex pattern will expire:** the regex used to determine what tags should be expired. To qualify all tags for expiration, use the default value of `.*`.
|
||||
|
||||
### Managing project expiration policy through the API
|
||||
|
||||
You can set, update, and disable the expiration policies using the GitLab API.
|
||||
|
@ -368,20 +498,6 @@ Examples:
|
|||
|
||||
See the API documentation for further details: [Edit project](../../../api/projects.md#edit-project).
|
||||
|
||||
### Managing project expiration policy through the UI
|
||||
|
||||
To manage project expiration policy, navigate to **Settings > CI/CD > Container Registry tag expiration policy**.
|
||||
|
||||
![Expiration Policy App](img/expiration-policy-app.png)
|
||||
|
||||
The UI allows you to configure the following:
|
||||
|
||||
- **Expiration policy:** enable or disable the expiration policy.
|
||||
- **Expiration interval:** how long tags are exempt from being deleted.
|
||||
- **Expiration schedule:** how often the cron job checking the tags should run.
|
||||
- **Expiration latest:** how many tags to _always_ keep for each image.
|
||||
- **Docker tags with names matching this regex pattern will expire:** the regex used to determine what tags should be expired. To qualify all tags for expiration, use the default value of `.*`.
|
||||
|
||||
## Limitations
|
||||
|
||||
Moving or renaming existing Container Registry repositories is not supported
|
||||
|
|
|
@ -53,10 +53,10 @@ module API
|
|||
|
||||
params do
|
||||
requires :name, type: String, desc: "New deploy token's name"
|
||||
requires :expires_at, type: DateTime, desc: 'Expiration date for the deploy token. Does not expire if no value is provided.'
|
||||
requires :username, type: String, desc: 'Username for deploy token. Default is `gitlab+deploy-token-{n}`'
|
||||
requires :scopes, type: Array[String], values: ::DeployToken::AVAILABLE_SCOPES.map(&:to_s),
|
||||
desc: 'Indicates the deploy token scopes. Must be at least one of "read_repository" or "read_registry".'
|
||||
optional :expires_at, type: DateTime, desc: 'Expiration date for the deploy token. Does not expire if no value is provided.'
|
||||
optional :username, type: String, desc: 'Username for deploy token. Default is `gitlab+deploy-token-{n}`'
|
||||
end
|
||||
desc 'Create a project deploy token' do
|
||||
detail 'This feature was introduced in GitLab 12.9'
|
||||
|
@ -114,10 +114,10 @@ module API
|
|||
|
||||
params do
|
||||
requires :name, type: String, desc: 'The name of the deploy token'
|
||||
requires :expires_at, type: DateTime, desc: 'Expiration date for the deploy token. Does not expire if no value is provided.'
|
||||
requires :username, type: String, desc: 'Username for deploy token. Default is `gitlab+deploy-token-{n}`'
|
||||
requires :scopes, type: Array[String], values: ::DeployToken::AVAILABLE_SCOPES.map(&:to_s),
|
||||
desc: 'Indicates the deploy token scopes. Must be at least one of "read_repository" or "read_registry".'
|
||||
optional :expires_at, type: DateTime, desc: 'Expiration date for the deploy token. Does not expire if no value is provided.'
|
||||
optional :username, type: String, desc: 'Username for deploy token. Default is `gitlab+deploy-token-{n}`'
|
||||
end
|
||||
desc 'Create a group deploy token' do
|
||||
detail 'This feature was introduced in GitLab 12.9'
|
||||
|
|
|
@ -367,6 +367,10 @@ module API
|
|||
render_api_error!('405 Method Not Allowed', 405)
|
||||
end
|
||||
|
||||
def not_acceptable!
|
||||
render_api_error!('406 Not Acceptable', 406)
|
||||
end
|
||||
|
||||
def service_unavailable!
|
||||
render_api_error!('503 Service Unavailable', 503)
|
||||
end
|
||||
|
|
|
@ -95,6 +95,8 @@ module API
|
|||
render_api_error!({ error: ::Gitlab::RateLimitHelpers::ARCHIVE_RATE_LIMIT_REACHED_MESSAGE }, 429)
|
||||
end
|
||||
|
||||
not_acceptable! if Gitlab::HotlinkingDetector.intercept_hotlinking?(request)
|
||||
|
||||
send_git_archive user_project.repository, ref: params[:sha], format: params[:format], append_sha: true
|
||||
rescue
|
||||
not_found!('File')
|
||||
|
|
|
@ -74,6 +74,8 @@ module API
|
|||
desc: 'The visibility of the snippet'
|
||||
end
|
||||
post do
|
||||
authorize! :create_snippet
|
||||
|
||||
attrs = declared_params(include_missing: false).merge(request: request, api: true)
|
||||
service_response = ::Snippets::CreateService.new(nil, current_user, attrs).execute
|
||||
snippet = service_response.payload[:snippet]
|
||||
|
|
|
@ -109,6 +109,8 @@ module API
|
|||
trigger = user_project.triggers.find(params.delete(:trigger_id))
|
||||
break not_found!('Trigger') unless trigger
|
||||
|
||||
authorize! :admin_trigger, trigger
|
||||
|
||||
if trigger.update(declared_params(include_missing: false))
|
||||
present trigger, with: Entities::Trigger, current_user: current_user
|
||||
else
|
||||
|
|
|
@ -171,6 +171,8 @@ module Gitlab
|
|||
|
||||
if valid_oauth_token?(token)
|
||||
user = User.find_by(id: token.resource_owner_id)
|
||||
return unless user.can?(:log_in)
|
||||
|
||||
Gitlab::Auth::Result.new(user, nil, :oauth, full_authentication_abilities)
|
||||
end
|
||||
end
|
||||
|
@ -182,7 +184,7 @@ module Gitlab
|
|||
|
||||
token = PersonalAccessTokensFinder.new(state: 'active').find_by_token(password)
|
||||
|
||||
if token && valid_scoped_token?(token, all_available_scopes)
|
||||
if token && valid_scoped_token?(token, all_available_scopes) && token.user.can?(:log_in)
|
||||
Gitlab::Auth::Result.new(token.user, nil, :personal_access_token, abilities_for_scopes(token.scopes))
|
||||
end
|
||||
end
|
||||
|
@ -260,6 +262,8 @@ module Gitlab
|
|||
return unless build.project.builds_enabled?
|
||||
|
||||
if build.user
|
||||
return unless build.user.can?(:log_in)
|
||||
|
||||
# If user is assigned to build, use restricted credentials of user
|
||||
Gitlab::Auth::Result.new(build.user, build.project, :build, build_authentication_abilities)
|
||||
else
|
||||
|
|
|
@ -22,6 +22,8 @@ module Gitlab
|
|||
return @text unless needs_rewrite?
|
||||
|
||||
@text.gsub(@pattern) do |markdown|
|
||||
Gitlab::Utils.check_path_traversal!($~[:file])
|
||||
|
||||
file = find_file(@source_project, $~[:secret], $~[:file])
|
||||
break markdown unless file.try(:exists?)
|
||||
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
class HotlinkingDetector
|
||||
IMAGE_FORMATS = %w(image/jpeg image/apng image/png image/webp image/svg+xml image/*).freeze
|
||||
MEDIA_FORMATS = %w(video/webm video/ogg video/* application/ogg audio/webm audio/ogg audio/wav audio/*).freeze
|
||||
CSS_FORMATS = %w(text/css).freeze
|
||||
INVALID_FORMATS = (IMAGE_FORMATS + MEDIA_FORMATS + CSS_FORMATS).freeze
|
||||
INVALID_FETCH_MODES = %w(cors no-cors websocket).freeze
|
||||
|
||||
class << self
|
||||
def intercept_hotlinking?(request)
|
||||
request_accepts = parse_request_accepts(request)
|
||||
|
||||
return false unless Feature.enabled?(:repository_archive_hotlinking_interception, default_enabled: true)
|
||||
|
||||
# Block attempts to embed as JS
|
||||
return true if sec_fetch_invalid?(request)
|
||||
|
||||
# If no Accept header was set, skip the rest
|
||||
return false if request_accepts.empty?
|
||||
|
||||
# Workaround for IE8 weirdness
|
||||
return false if IMAGE_FORMATS.include?(request_accepts.first) && request_accepts.include?("application/x-ms-application")
|
||||
|
||||
# Block all other media requests if the first format is a media type
|
||||
return true if INVALID_FORMATS.include?(request_accepts.first)
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def sec_fetch_invalid?(request)
|
||||
fetch_mode = request.headers["Sec-Fetch-Mode"]
|
||||
|
||||
return if fetch_mode.blank?
|
||||
return true if INVALID_FETCH_MODES.include?(fetch_mode)
|
||||
end
|
||||
|
||||
def parse_request_accepts(request)
|
||||
# Rails will already have parsed the Accept header
|
||||
return request.accepts if request.respond_to?(:accepts)
|
||||
|
||||
# Grape doesn't parse it, so we can use the Rails system for this
|
||||
return Mime::Type.parse(request.headers["Accept"]) if request.respond_to?(:headers) && request.headers["Accept"].present?
|
||||
|
||||
[]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -11,7 +11,14 @@ module Gitlab
|
|||
'discussion_id',
|
||||
'custom_attributes'
|
||||
].freeze
|
||||
PROHIBITED_REFERENCES = Regexp.union(/\Acached_markdown_version\Z/, /_id\Z/, /_ids\Z/, /_html\Z/, /attributes/).freeze
|
||||
PROHIBITED_REFERENCES = Regexp.union(
|
||||
/\Acached_markdown_version\Z/,
|
||||
/_id\Z/,
|
||||
/_ids\Z/,
|
||||
/_html\Z/,
|
||||
/attributes/,
|
||||
/\Aremote_\w+_(url|urls|request_header)\Z/ # carrierwave automatically creates these attribute methods for uploads
|
||||
).freeze
|
||||
|
||||
def self.clean(*args)
|
||||
new(*args).clean
|
||||
|
|
|
@ -4,12 +4,13 @@ module Gitlab
|
|||
module ImportExport
|
||||
module Group
|
||||
class TreeRestorer
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
attr_reader :user
|
||||
attr_reader :shared
|
||||
attr_reader :group
|
||||
|
||||
def initialize(user:, shared:, group:, group_hash:)
|
||||
@path = File.join(shared.export_path, 'group.json')
|
||||
@user = user
|
||||
@shared = shared
|
||||
@group = group
|
||||
|
@ -17,17 +18,14 @@ module Gitlab
|
|||
end
|
||||
|
||||
def restore
|
||||
@relation_reader ||=
|
||||
if @group_hash.present?
|
||||
ImportExport::JSON::LegacyReader::User.new(@group_hash, reader.group_relation_names)
|
||||
else
|
||||
ImportExport::JSON::LegacyReader::File.new(@path, reader.group_relation_names)
|
||||
end
|
||||
@group_attributes = relation_reader.consume_attributes(nil)
|
||||
@group_members = relation_reader.consume_relation(nil, 'members')
|
||||
|
||||
@group_members = @relation_reader.consume_relation('members')
|
||||
@children = @relation_reader.consume_attribute('children')
|
||||
@relation_reader.consume_attribute('name')
|
||||
@relation_reader.consume_attribute('path')
|
||||
# We need to remove `name` and `path` as we did consume it in previous pass
|
||||
@group_attributes.delete('name')
|
||||
@group_attributes.delete('path')
|
||||
|
||||
@children = @group_attributes.delete('children')
|
||||
|
||||
if members_mapper.map && restorer.restore
|
||||
@children&.each do |group_hash|
|
||||
|
@ -53,16 +51,32 @@ module Gitlab
|
|||
|
||||
private
|
||||
|
||||
def relation_reader
|
||||
strong_memoize(:relation_reader) do
|
||||
if @group_hash.present?
|
||||
ImportExport::JSON::LegacyReader::Hash.new(
|
||||
@group_hash,
|
||||
relation_names: reader.group_relation_names)
|
||||
else
|
||||
ImportExport::JSON::LegacyReader::File.new(
|
||||
File.join(shared.export_path, 'group.json'),
|
||||
relation_names: reader.group_relation_names)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def restorer
|
||||
@relation_tree_restorer ||= RelationTreeRestorer.new(
|
||||
user: @user,
|
||||
shared: @shared,
|
||||
importable: @group,
|
||||
relation_reader: @relation_reader,
|
||||
members_mapper: members_mapper,
|
||||
object_builder: object_builder,
|
||||
relation_factory: relation_factory,
|
||||
reader: reader
|
||||
user: @user,
|
||||
shared: @shared,
|
||||
relation_reader: relation_reader,
|
||||
members_mapper: members_mapper,
|
||||
object_builder: object_builder,
|
||||
relation_factory: relation_factory,
|
||||
reader: reader,
|
||||
importable: @group,
|
||||
importable_attributes: @group_attributes,
|
||||
importable_path: nil
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -88,7 +102,11 @@ module Gitlab
|
|||
end
|
||||
|
||||
def members_mapper
|
||||
@members_mapper ||= Gitlab::ImportExport::MembersMapper.new(exported_members: @group_members, user: @user, importable: @group)
|
||||
@members_mapper ||= Gitlab::ImportExport::MembersMapper.new(
|
||||
exported_members: @group_members,
|
||||
user: @user,
|
||||
importable: @group
|
||||
)
|
||||
end
|
||||
|
||||
def relation_factory
|
||||
|
|
|
@ -5,19 +5,25 @@ module Gitlab
|
|||
module JSON
|
||||
class LegacyReader
|
||||
class File < LegacyReader
|
||||
def initialize(path, relation_names)
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
def initialize(path, relation_names:, allowed_path: nil)
|
||||
@path = path
|
||||
super(relation_names)
|
||||
super(
|
||||
relation_names: relation_names,
|
||||
allowed_path: allowed_path)
|
||||
end
|
||||
|
||||
def valid?
|
||||
def exist?
|
||||
::File.exist?(@path)
|
||||
end
|
||||
|
||||
private
|
||||
protected
|
||||
|
||||
def tree_hash
|
||||
@tree_hash ||= read_hash
|
||||
strong_memoize(:tree_hash) do
|
||||
read_hash
|
||||
end
|
||||
end
|
||||
|
||||
def read_hash
|
||||
|
@ -28,13 +34,15 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
class User < LegacyReader
|
||||
def initialize(tree_hash, relation_names)
|
||||
class Hash < LegacyReader
|
||||
def initialize(tree_hash, relation_names:, allowed_path: nil)
|
||||
@tree_hash = tree_hash
|
||||
super(relation_names)
|
||||
super(
|
||||
relation_names: relation_names,
|
||||
allowed_path: allowed_path)
|
||||
end
|
||||
|
||||
def valid?
|
||||
def exist?
|
||||
@tree_hash.present?
|
||||
end
|
||||
|
||||
|
@ -43,11 +51,16 @@ module Gitlab
|
|||
attr_reader :tree_hash
|
||||
end
|
||||
|
||||
def initialize(relation_names)
|
||||
def initialize(relation_names:, allowed_path:)
|
||||
@relation_names = relation_names.map(&:to_s)
|
||||
|
||||
# This is legacy reader, to be used in transition
|
||||
# period before `.ndjson`,
|
||||
# we strong validate what is being readed
|
||||
@allowed_path = allowed_path
|
||||
end
|
||||
|
||||
def valid?
|
||||
def exist?
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
|
@ -55,15 +68,22 @@ module Gitlab
|
|||
true
|
||||
end
|
||||
|
||||
def root_attributes(excluded_attributes = [])
|
||||
attributes.except(*excluded_attributes.map(&:to_s))
|
||||
def consume_attributes(importable_path)
|
||||
unless importable_path == @allowed_path
|
||||
raise ArgumentError, "Invalid #{importable_path} passed to `consume_attributes`. Use #{@allowed_path} instead."
|
||||
end
|
||||
|
||||
attributes
|
||||
end
|
||||
|
||||
def consume_relation(key)
|
||||
def consume_relation(importable_path, key)
|
||||
unless importable_path == @allowed_path
|
||||
raise ArgumentError, "Invalid #{importable_name} passed to `consume_relation`. Use #{@allowed_path} instead."
|
||||
end
|
||||
|
||||
value = relations.delete(key)
|
||||
|
||||
return value unless block_given?
|
||||
|
||||
return if value.nil?
|
||||
|
||||
if value.is_a?(Array)
|
||||
|
@ -75,17 +95,13 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
def consume_attribute(key)
|
||||
attributes.delete(key)
|
||||
end
|
||||
|
||||
def sort_ci_pipelines_by_id
|
||||
relations['ci_pipelines']&.sort_by! { |hash| hash['id'] }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :relation_names
|
||||
attr_reader :relation_names, :allowed_path
|
||||
|
||||
def tree_hash
|
||||
raise NotImplementedError
|
||||
|
|
|
@ -8,11 +8,15 @@ module Gitlab
|
|||
|
||||
attr_reader :path
|
||||
|
||||
def initialize(path)
|
||||
def initialize(path, allowed_path:)
|
||||
@path = path
|
||||
@last_array = nil
|
||||
@keys = Set.new
|
||||
|
||||
# This is legacy writer, to be used in transition
|
||||
# period before `.ndjson`,
|
||||
# we strong validate what is being written
|
||||
@allowed_path = allowed_path
|
||||
|
||||
mkdir_p(File.dirname(@path))
|
||||
file.write('{}')
|
||||
end
|
||||
|
@ -22,12 +26,44 @@ module Gitlab
|
|||
@file = nil
|
||||
end
|
||||
|
||||
def set(hash)
|
||||
def write_attributes(exportable_path, hash)
|
||||
unless exportable_path == @allowed_path
|
||||
raise ArgumentError, "Invalid #{exportable_path}"
|
||||
end
|
||||
|
||||
hash.each do |key, value|
|
||||
write(key, value)
|
||||
end
|
||||
end
|
||||
|
||||
def write_relation(exportable_path, key, value)
|
||||
unless exportable_path == @allowed_path
|
||||
raise ArgumentError, "Invalid #{exportable_path}"
|
||||
end
|
||||
|
||||
write(key, value)
|
||||
end
|
||||
|
||||
def write_relation_array(exportable_path, key, items)
|
||||
unless exportable_path == @allowed_path
|
||||
raise ArgumentError, "Invalid #{exportable_path}"
|
||||
end
|
||||
|
||||
write(key, [])
|
||||
|
||||
# rewind by two bytes, to overwrite ']}'
|
||||
file.pos = file.size - 2
|
||||
|
||||
items.each_with_index do |item, idx|
|
||||
file.write(',') if idx > 0
|
||||
file.write(item.to_json)
|
||||
end
|
||||
|
||||
file.write(']}')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def write(key, value)
|
||||
raise ArgumentError, "key '#{key}' already written" if @keys.include?(key)
|
||||
|
||||
|
@ -41,29 +77,8 @@ module Gitlab
|
|||
file.write('}')
|
||||
|
||||
@keys.add(key)
|
||||
@last_array = nil
|
||||
@last_array_count = nil
|
||||
end
|
||||
|
||||
def append(key, value)
|
||||
unless @last_array == key
|
||||
write(key, [])
|
||||
|
||||
@last_array = key
|
||||
@last_array_count = 0
|
||||
end
|
||||
|
||||
# rewind by two bytes, to overwrite ']}'
|
||||
file.pos = file.size - 2
|
||||
|
||||
file.write(',') if @last_array_count > 0
|
||||
file.write(value.to_json)
|
||||
file.write(']}')
|
||||
@last_array_count += 1
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def file
|
||||
@file ||= File.open(@path, "wb")
|
||||
end
|
||||
|
|
|
@ -14,8 +14,9 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
def initialize(exportable, relations_schema, json_writer)
|
||||
def initialize(exportable, relations_schema, json_writer, exportable_path:)
|
||||
@exportable = exportable
|
||||
@exportable_path = exportable_path
|
||||
@relations_schema = relations_schema
|
||||
@json_writer = json_writer
|
||||
end
|
||||
|
@ -35,7 +36,7 @@ module Gitlab
|
|||
def serialize_root
|
||||
attributes = exportable.as_json(
|
||||
relations_schema.merge(include: nil, preloads: nil))
|
||||
json_writer.set(attributes)
|
||||
json_writer.write_attributes(@exportable_path, attributes)
|
||||
end
|
||||
|
||||
def serialize_relation(definition)
|
||||
|
@ -47,16 +48,28 @@ module Gitlab
|
|||
record = exportable.public_send(key) # rubocop: disable GitlabSecurity/PublicSend
|
||||
if record.is_a?(ActiveRecord::Relation)
|
||||
serialize_many_relations(key, record, options)
|
||||
elsif record.respond_to?(:each) # this is to support `project_members` that return an Array
|
||||
serialize_many_each(key, record, options)
|
||||
else
|
||||
serialize_single_relation(key, record, options)
|
||||
end
|
||||
end
|
||||
|
||||
def serialize_many_relations(key, records, options)
|
||||
key_preloads = preloads&.dig(key)
|
||||
records = records.preload(key_preloads) if key_preloads
|
||||
enumerator = Enumerator.new do |items|
|
||||
key_preloads = preloads&.dig(key)
|
||||
records = records.preload(key_preloads) if key_preloads
|
||||
|
||||
records.find_each(batch_size: BATCH_SIZE) do |record|
|
||||
records.find_each(batch_size: BATCH_SIZE) do |record|
|
||||
items << Raw.new(record.to_json(options))
|
||||
end
|
||||
end
|
||||
|
||||
json_writer.write_relation_array(@exportable_path, key, enumerator)
|
||||
end
|
||||
|
||||
def serialize_many_each(key, records, options)
|
||||
records.each do |record|
|
||||
json = Raw.new(record.to_json(options))
|
||||
|
||||
json_writer.append(key, json)
|
||||
|
@ -66,7 +79,7 @@ module Gitlab
|
|||
def serialize_single_relation(key, record, options)
|
||||
json = Raw.new(record.to_json(options))
|
||||
|
||||
json_writer.write(key, json)
|
||||
json_writer.write_relation(@exportable_path, key, json)
|
||||
end
|
||||
|
||||
def includes
|
||||
|
|
|
@ -4,6 +4,8 @@ module Gitlab
|
|||
module ImportExport
|
||||
module Project
|
||||
class TreeRestorer
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
attr_reader :user
|
||||
attr_reader :shared
|
||||
attr_reader :project
|
||||
|
@ -15,9 +17,8 @@ module Gitlab
|
|||
end
|
||||
|
||||
def restore
|
||||
@relation_reader = ImportExport::JSON::LegacyReader::File.new(File.join(shared.export_path, 'project.json'), reader.project_relation_names)
|
||||
|
||||
@project_members = @relation_reader.consume_relation('project_members')
|
||||
@project_attributes = relation_reader.consume_attributes(importable_path)
|
||||
@project_members = relation_reader.consume_relation(importable_path, 'project_members')
|
||||
|
||||
if relation_tree_restorer.restore
|
||||
import_failure_service.with_retry(action: 'set_latest_merge_request_diff_ids!') do
|
||||
|
@ -35,16 +36,28 @@ module Gitlab
|
|||
|
||||
private
|
||||
|
||||
def relation_reader
|
||||
strong_memoize(:relation_reader) do
|
||||
ImportExport::JSON::LegacyReader::File.new(
|
||||
File.join(shared.export_path, 'project.json'),
|
||||
relation_names: reader.project_relation_names,
|
||||
allowed_path: importable_path
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def relation_tree_restorer
|
||||
@relation_tree_restorer ||= RelationTreeRestorer.new(
|
||||
user: @user,
|
||||
shared: @shared,
|
||||
importable: @project,
|
||||
relation_reader: @relation_reader,
|
||||
relation_reader: relation_reader,
|
||||
object_builder: object_builder,
|
||||
members_mapper: members_mapper,
|
||||
relation_factory: relation_factory,
|
||||
reader: reader
|
||||
reader: reader,
|
||||
importable: @project,
|
||||
importable_attributes: @project_attributes,
|
||||
importable_path: importable_path
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -69,6 +82,10 @@ module Gitlab
|
|||
def import_failure_service
|
||||
@import_failure_service ||= ImportFailureService.new(@project)
|
||||
end
|
||||
|
||||
def importable_path
|
||||
"project"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -15,10 +15,17 @@ module Gitlab
|
|||
end
|
||||
|
||||
def save
|
||||
json_writer = ImportExport::JSON::LegacyWriter.new(@full_path)
|
||||
json_writer = ImportExport::JSON::LegacyWriter.new(
|
||||
@full_path,
|
||||
allowed_path: "project"
|
||||
)
|
||||
|
||||
serializer = ImportExport::JSON::StreamingSerializer.new(exportable, reader.project_tree, json_writer)
|
||||
serializer.execute
|
||||
ImportExport::JSON::StreamingSerializer.new(
|
||||
exportable,
|
||||
reader.project_tree,
|
||||
json_writer,
|
||||
exportable_path: "project"
|
||||
).execute
|
||||
|
||||
true
|
||||
rescue => e
|
||||
|
|
|
@ -11,7 +11,15 @@ module Gitlab
|
|||
attr_reader :importable
|
||||
attr_reader :relation_reader
|
||||
|
||||
def initialize(user:, shared:, importable:, relation_reader:, members_mapper:, object_builder:, relation_factory:, reader:)
|
||||
def initialize( # rubocop:disable Metrics/ParameterLists
|
||||
user:, shared:, relation_reader:,
|
||||
members_mapper:, object_builder:,
|
||||
relation_factory:,
|
||||
reader:,
|
||||
importable:,
|
||||
importable_attributes:,
|
||||
importable_path:
|
||||
)
|
||||
@user = user
|
||||
@shared = shared
|
||||
@importable = importable
|
||||
|
@ -20,6 +28,8 @@ module Gitlab
|
|||
@object_builder = object_builder
|
||||
@relation_factory = relation_factory
|
||||
@reader = reader
|
||||
@importable_attributes = importable_attributes
|
||||
@importable_path = importable_path
|
||||
end
|
||||
|
||||
def restore
|
||||
|
@ -57,7 +67,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def process_relation!(relation_key, relation_definition)
|
||||
@relation_reader.consume_relation(relation_key) do |data_hash, relation_index|
|
||||
@relation_reader.consume_relation(@importable_path, relation_key) do |data_hash, relation_index|
|
||||
process_relation_item!(relation_key, relation_definition, relation_index, data_hash)
|
||||
end
|
||||
end
|
||||
|
@ -94,7 +104,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def update_params!
|
||||
params = @relation_reader.root_attributes(relations.keys)
|
||||
params = @importable_attributes.except(*relations.keys.map(&:to_s))
|
||||
params = params.merge(present_override_params)
|
||||
|
||||
# Cleaning all imported and overridden params
|
||||
|
|
|
@ -16,6 +16,14 @@ module Gitlab
|
|||
"It must start with letter, digit, emoji or '_'."
|
||||
end
|
||||
|
||||
def group_name_regex
|
||||
project_name_regex
|
||||
end
|
||||
|
||||
def group_name_regex_message
|
||||
project_name_regex_message
|
||||
end
|
||||
|
||||
##
|
||||
# Docker Distribution Registry repository / tag name rules
|
||||
#
|
||||
|
|
|
@ -12860,6 +12860,9 @@ msgstr ""
|
|||
msgid "Mirror repository"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mirror settings are only available to GitLab administrators."
|
||||
msgstr ""
|
||||
|
||||
msgid "Mirror user"
|
||||
msgstr ""
|
||||
|
||||
|
@ -18862,6 +18865,9 @@ msgstr ""
|
|||
msgid "Specified URL cannot be used."
|
||||
msgstr ""
|
||||
|
||||
msgid "Specified URL cannot be used: \"%{reason}\""
|
||||
msgstr ""
|
||||
|
||||
msgid "Specify an e-mail address regex pattern to identify default internal users."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
require 'nokogiri'
|
||||
|
||||
module QA
|
||||
context 'Manage' do
|
||||
context 'Manage', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/212145', type: :stale } do
|
||||
describe 'Check for broken images', :requires_admin do
|
||||
before(:context) do
|
||||
admin = QA::Resource::User.new.tap do |user|
|
||||
|
|
|
@ -258,6 +258,18 @@ describe GroupsController do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "malicious group name" do
|
||||
subject { post :create, params: { group: { name: "<script>alert('Mayday!');</script>", path: "invalid_group_url" } } }
|
||||
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it { expect { subject }.not_to change { Group.count } }
|
||||
|
||||
it { expect(subject).to render_template(:new) }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET #index' do
|
||||
|
@ -836,6 +848,16 @@ describe GroupsController do
|
|||
put :update, params: { id: group.to_param, group: { name: 'world' } }
|
||||
end.to change { group.reload.name }
|
||||
end
|
||||
|
||||
context "malicious group name" do
|
||||
subject { put :update, params: { id: group.to_param, group: { name: "<script>alert('Attack!');</script>" } } }
|
||||
|
||||
it { is_expected.to render_template(:edit) }
|
||||
|
||||
it 'does not update name' do
|
||||
expect { subject }.not_to change { group.reload.name }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE #destroy' do
|
||||
|
|
|
@ -25,6 +25,35 @@ describe Import::FogbugzController do
|
|||
expect(session[:fogbugz_uri]).to eq(uri)
|
||||
expect(response).to redirect_to(new_user_map_import_fogbugz_path)
|
||||
end
|
||||
|
||||
context 'verify url' do
|
||||
shared_examples 'denies local request' do |reason|
|
||||
it 'does not allow requests' do
|
||||
post :callback, params: { uri: uri, email: 'test@example.com', password: 'mypassword' }
|
||||
|
||||
expect(response).to redirect_to(new_import_fogbugz_url)
|
||||
expect(flash[:alert]).to eq("Specified URL cannot be used: \"#{reason}\"")
|
||||
end
|
||||
end
|
||||
|
||||
context 'when host is localhost' do
|
||||
let(:uri) { 'https://localhost:3000' }
|
||||
|
||||
include_examples 'denies local request', 'Requests to localhost are not allowed'
|
||||
end
|
||||
|
||||
context 'when host is on local network' do
|
||||
let(:uri) { 'http://192.168.0.1/' }
|
||||
|
||||
include_examples 'denies local request', 'Requests to the local network are not allowed'
|
||||
end
|
||||
|
||||
context 'when host is ftp protocol' do
|
||||
let(:uri) { 'ftp://testing' }
|
||||
|
||||
include_examples 'denies local request', 'Only allowed schemes are http, https'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST #create_user_map' do
|
||||
|
|
|
@ -5,6 +5,72 @@ require 'spec_helper'
|
|||
describe Projects::MirrorsController do
|
||||
include ReactiveCachingHelpers
|
||||
|
||||
shared_examples 'only admin is allowed when mirroring is disabled' do
|
||||
let(:subject_action) { raise 'subject_action is required' }
|
||||
let(:user) { project.owner }
|
||||
let(:project_settings_path) { project_settings_repository_path(project, anchor: 'js-push-remote-settings') }
|
||||
|
||||
context 'when project mirroring is enabled' do
|
||||
it 'allows requests from a maintainer' do
|
||||
sign_in(user)
|
||||
|
||||
subject_action
|
||||
expect(response).to redirect_to(project_settings_path)
|
||||
end
|
||||
|
||||
it 'allows requests from an admin user' do
|
||||
user.update!(admin: true)
|
||||
sign_in(user)
|
||||
|
||||
subject_action
|
||||
expect(response).to redirect_to(project_settings_path)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when project mirroring is disabled' do
|
||||
before do
|
||||
stub_application_setting(mirror_available: false)
|
||||
end
|
||||
|
||||
it 'disallows requests from a maintainer' do
|
||||
sign_in(user)
|
||||
|
||||
subject_action
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
|
||||
it 'allows requests from an admin user' do
|
||||
user.update!(admin: true)
|
||||
sign_in(user)
|
||||
|
||||
subject_action
|
||||
expect(response).to redirect_to(project_settings_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Access control' do
|
||||
let(:project) { create(:project, :repository) }
|
||||
|
||||
describe '#update' do
|
||||
include_examples 'only admin is allowed when mirroring is disabled' do
|
||||
let(:subject_action) do
|
||||
do_put(project, remote_mirrors_attributes: { '0' => { 'enabled' => 1, 'url' => 'http://foo.com' } })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#update_now' do
|
||||
include_examples 'only admin is allowed when mirroring is disabled' do
|
||||
let(:options) { { namespace_id: project.namespace, project_id: project } }
|
||||
|
||||
let(:subject_action) do
|
||||
get :update_now, params: options.merge(sync_remote: true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'setting up a remote mirror' do
|
||||
let_it_be(:project) { create(:project, :repository) }
|
||||
|
||||
|
|
|
@ -28,6 +28,12 @@ describe Projects::RepositoriesController do
|
|||
sign_in(user)
|
||||
end
|
||||
|
||||
it_behaves_like "hotlink interceptor" do
|
||||
let(:http_request) do
|
||||
get :archive, params: { namespace_id: project.namespace, project_id: project, id: "master" }, format: "zip"
|
||||
end
|
||||
end
|
||||
|
||||
it "uses Gitlab::Workhorse" do
|
||||
get :archive, params: { namespace_id: project.namespace, project_id: project, id: "master" }, format: "zip"
|
||||
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
shared_examples "installing applications on a cluster" do
|
||||
shared_examples "installing applications for a cluster" do |managed_apps_local_tiller|
|
||||
before do
|
||||
stub_feature_flags(managed_apps_local_tiller: managed_apps_local_tiller)
|
||||
|
||||
visit cluster_path
|
||||
end
|
||||
|
||||
|
@ -26,48 +28,61 @@ shared_examples "installing applications on a cluster" do
|
|||
it 'user can install applications' do
|
||||
wait_for_requests
|
||||
|
||||
page.within('.js-cluster-application-row-helm') do
|
||||
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to be_nil
|
||||
application_row =
|
||||
if managed_apps_local_tiller
|
||||
'.js-cluster-application-row-ingress'
|
||||
else
|
||||
'.js-cluster-application-row-helm'
|
||||
end
|
||||
|
||||
page.within(application_row) do
|
||||
expect(page).not_to have_css('.js-cluster-application-install-button[disabled]')
|
||||
expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Install')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user installs Helm' do
|
||||
before do
|
||||
allow(ClusterInstallAppWorker).to receive(:perform_async)
|
||||
wait_for_requests
|
||||
|
||||
page.within('.js-cluster-application-row-helm') do
|
||||
page.find(:css, '.js-cluster-application-install-button').click
|
||||
end
|
||||
|
||||
wait_for_requests
|
||||
if managed_apps_local_tiller
|
||||
it 'does not show the Helm application' do
|
||||
expect(page).not_to have_selector(:css, '.js-cluster-application-row-helm')
|
||||
end
|
||||
else
|
||||
context 'when user installs Helm' do
|
||||
before do
|
||||
allow(ClusterInstallAppWorker).to receive(:perform_async)
|
||||
wait_for_requests
|
||||
|
||||
it 'shows the status transition' do
|
||||
page.within('.js-cluster-application-row-helm') do
|
||||
# FE sends request and gets the response, then the buttons is "Installing"
|
||||
expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installing')
|
||||
page.within('.js-cluster-application-row-helm') do
|
||||
page.find(:css, '.js-cluster-application-install-button').click
|
||||
end
|
||||
|
||||
Clusters::Cluster.last.application_helm.make_installing!
|
||||
|
||||
# FE starts polling and update the buttons to "Installing"
|
||||
expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installing')
|
||||
|
||||
Clusters::Cluster.last.application_helm.make_installed!
|
||||
|
||||
expect(page).not_to have_css('button', exact_text: 'Install', visible: :all)
|
||||
expect(page).not_to have_css('button', exact_text: 'Installing', visible: :all)
|
||||
expect(page).to have_css('.js-cluster-application-uninstall-button:not([disabled])', exact_text: 'Uninstall')
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
expect(page).to have_content('Helm Tiller was successfully installed on your Kubernetes cluster')
|
||||
it 'shows the status transition' do
|
||||
page.within('.js-cluster-application-row-helm') do
|
||||
# FE sends request and gets the response, then the buttons is "Installing"
|
||||
expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installing')
|
||||
|
||||
Clusters::Cluster.last.application_helm.make_installing!
|
||||
|
||||
# FE starts polling and update the buttons to "Installing"
|
||||
expect(page).to have_css('.js-cluster-application-install-button[disabled]', exact_text: 'Installing')
|
||||
|
||||
Clusters::Cluster.last.application_helm.make_installed!
|
||||
|
||||
expect(page).not_to have_css('button', exact_text: 'Install', visible: :all)
|
||||
expect(page).not_to have_css('button', exact_text: 'Installing', visible: :all)
|
||||
expect(page).to have_css('.js-cluster-application-uninstall-button:not([disabled])', exact_text: 'Uninstall')
|
||||
end
|
||||
|
||||
expect(page).to have_content('Helm Tiller was successfully installed on your Kubernetes cluster')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user installs Knative' do
|
||||
before do
|
||||
create(:clusters_applications_helm, :installed, cluster: cluster)
|
||||
create(:clusters_applications_helm, :installed, cluster: cluster) unless managed_apps_local_tiller
|
||||
end
|
||||
|
||||
context 'on an abac cluster' do
|
||||
|
@ -153,7 +168,7 @@ shared_examples "installing applications on a cluster" do
|
|||
allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in)
|
||||
allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async)
|
||||
|
||||
create(:clusters_applications_helm, :installed, cluster: cluster)
|
||||
create(:clusters_applications_helm, :installed, cluster: cluster) unless managed_apps_local_tiller
|
||||
|
||||
page.within('.js-cluster-application-row-cert_manager') do
|
||||
click_button 'Install'
|
||||
|
@ -189,7 +204,7 @@ shared_examples "installing applications on a cluster" do
|
|||
before do
|
||||
allow(ClusterInstallAppWorker).to receive(:perform_async)
|
||||
|
||||
create(:clusters_applications_helm, :installed, cluster: cluster)
|
||||
create(:clusters_applications_helm, :installed, cluster: cluster) unless managed_apps_local_tiller
|
||||
|
||||
page.within('.js-cluster-application-row-elastic_stack') do
|
||||
click_button 'Install'
|
||||
|
@ -221,7 +236,7 @@ shared_examples "installing applications on a cluster" do
|
|||
allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in)
|
||||
allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async)
|
||||
|
||||
create(:clusters_applications_helm, :installed, cluster: cluster)
|
||||
create(:clusters_applications_helm, :installed, cluster: cluster) unless managed_apps_local_tiller
|
||||
|
||||
page.within('.js-cluster-application-row-ingress') do
|
||||
expect(page).to have_css('.js-cluster-application-install-button:not([disabled])')
|
||||
|
@ -263,3 +278,8 @@ shared_examples "installing applications on a cluster" do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples "installing applications on a cluster" do
|
||||
it_behaves_like "installing applications for a cluster", false
|
||||
it_behaves_like "installing applications for a cluster", true
|
||||
end
|
||||
|
|
|
@ -26,11 +26,7 @@ describe 'Projects > Settings > Repository settings' do
|
|||
let(:role) { :maintainer }
|
||||
|
||||
context 'remote mirror settings' do
|
||||
let(:user2) { create(:user) }
|
||||
|
||||
before do
|
||||
project.add_maintainer(user2)
|
||||
|
||||
visit project_settings_repository_path(project)
|
||||
end
|
||||
|
||||
|
@ -90,6 +86,18 @@ describe 'Projects > Settings > Repository settings' do
|
|||
expect(page).to have_selector('[title="Copy SSH public key"]')
|
||||
end
|
||||
|
||||
context 'when project mirroring is disabled' do
|
||||
before do
|
||||
stub_application_setting(mirror_available: false)
|
||||
visit project_settings_repository_path(project)
|
||||
end
|
||||
|
||||
it 'hides remote mirror settings' do
|
||||
expect(page.find('.project-mirror-settings')).not_to have_selector('form')
|
||||
expect(page).to have_content('Mirror settings are only available to GitLab administrators.')
|
||||
end
|
||||
end
|
||||
|
||||
def select_direction(direction = 'push')
|
||||
direction_select = find('#mirror_direction')
|
||||
|
||||
|
@ -154,4 +162,31 @@ describe 'Projects > Settings > Repository settings' do
|
|||
expect(mirror).not_to have_selector('.rspec-update-now-button')
|
||||
end
|
||||
end
|
||||
|
||||
context 'for admin' do
|
||||
shared_examples_for 'shows mirror settings' do
|
||||
it 'shows mirror settings' do
|
||||
expect(page.find('.project-mirror-settings')).to have_selector('form')
|
||||
expect(page).not_to have_content('Changing mirroring setting is disabled for non-admin users.')
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
stub_application_setting(mirror_available: mirror_available)
|
||||
user.update!(admin: true)
|
||||
visit project_settings_repository_path(project)
|
||||
end
|
||||
|
||||
context 'when project mirroring is enabled' do
|
||||
let(:mirror_available) { true }
|
||||
|
||||
include_examples 'shows mirror settings'
|
||||
end
|
||||
|
||||
context 'when project mirroring is disabled' do
|
||||
let(:mirror_available) { false }
|
||||
|
||||
include_examples 'shows mirror settings'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -15,6 +15,9 @@ describe('Applications', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
Applications = Vue.extend(applications);
|
||||
|
||||
gon.features = gon.features || {};
|
||||
gon.features.managedAppsLocalTiller = false;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -156,6 +159,22 @@ describe('Applications', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Helm application', () => {
|
||||
describe('when managedAppsLocalTiller enabled', () => {
|
||||
beforeEach(() => {
|
||||
gon.features.managedAppsLocalTiller = true;
|
||||
});
|
||||
|
||||
it('does not render a row for Helm Tiller', () => {
|
||||
vm = mountComponent(Applications, {
|
||||
applications: APPLICATIONS_MOCK_STATE,
|
||||
});
|
||||
|
||||
expect(vm.$el.querySelector('.js-cluster-application-row-helm')).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Ingress application', () => {
|
||||
describe('with nested component', () => {
|
||||
const propsData = {
|
||||
|
|
|
@ -5,10 +5,10 @@ require 'spec_helper'
|
|||
describe Projects::AutocompleteSourcesController, '(JavaScript fixtures)', type: :controller do
|
||||
include JavaScriptFixturesHelpers
|
||||
|
||||
set(:admin) { create(:admin) }
|
||||
set(:group) { create(:group, name: 'frontend-fixtures') }
|
||||
set(:project) { create(:project, namespace: group, path: 'autocomplete-sources-project') }
|
||||
set(:issue) { create(:issue, project: project) }
|
||||
let_it_be(:admin) { create(:admin) }
|
||||
let_it_be(:group) { create(:group, name: 'frontend-fixtures') }
|
||||
let_it_be(:project) { create(:project, namespace: group, path: 'autocomplete-sources-project') }
|
||||
let_it_be(:issue) { create(:issue, project: project) }
|
||||
|
||||
before(:all) do
|
||||
clean_frontend_fixtures('autocomplete_sources/')
|
||||
|
|
|
@ -5,9 +5,9 @@ require 'spec_helper'
|
|||
describe Projects::CommitController, '(JavaScript fixtures)', type: :controller do
|
||||
include JavaScriptFixturesHelpers
|
||||
|
||||
set(:project) { create(:project, :repository) }
|
||||
set(:user) { create(:user) }
|
||||
let(:commit) { project.commit("master") }
|
||||
let_it_be(:project) { create(:project, :repository) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
let(:commit) { project.commit("master") }
|
||||
|
||||
render_views
|
||||
|
||||
|
|
|
@ -108,5 +108,23 @@ describe('Frequent Items utils spec', () => {
|
|||
|
||||
expect(sanitizeItem(input)).toEqual({ name: 'test', namespace: 'test', id: 1 });
|
||||
});
|
||||
|
||||
it("skips `name` key if it doesn't exist on the item", () => {
|
||||
const input = {
|
||||
namespace: '<br>test',
|
||||
id: 1,
|
||||
};
|
||||
|
||||
expect(sanitizeItem(input)).toEqual({ namespace: 'test', id: 1 });
|
||||
});
|
||||
|
||||
it("skips `namespace` key if it doesn't exist on the item", () => {
|
||||
const input = {
|
||||
name: '<br><b>test</b>',
|
||||
id: 1,
|
||||
};
|
||||
|
||||
expect(sanitizeItem(input)).toEqual({ name: 'test', id: 1 });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -523,7 +523,12 @@ describe Banzai::Filter::LabelReferenceFilter do
|
|||
end
|
||||
|
||||
context 'when group name has HTML entities' do
|
||||
let(:another_group) { create(:group, name: '<img src=x onerror=alert(1)>', path: 'another_group') }
|
||||
let(:another_group) { create(:group, name: 'random', path: 'another_group') }
|
||||
|
||||
before do
|
||||
another_group.name = "<img src=x onerror=alert(1)>"
|
||||
another_group.save!(validate: false)
|
||||
end
|
||||
|
||||
it 'escapes the HTML entities' do
|
||||
expect(result.text)
|
||||
|
|
|
@ -20,8 +20,8 @@ describe Banzai::Filter::ReferenceRedactorFilter do
|
|||
it 'skips when the skip_redaction flag is set' do
|
||||
user = create(:user)
|
||||
project = create(:project)
|
||||
|
||||
link = reference_link(project: project.id, reference_type: 'test')
|
||||
|
||||
doc = filter(link, current_user: user, skip_redaction: true)
|
||||
|
||||
expect(doc.css('a').length).to eq 1
|
||||
|
@ -51,8 +51,8 @@ describe Banzai::Filter::ReferenceRedactorFilter do
|
|||
user = create(:user)
|
||||
project = create(:project)
|
||||
project.add_maintainer(user)
|
||||
|
||||
link = reference_link(project: project.id, reference_type: 'test')
|
||||
|
||||
doc = filter(link, current_user: user)
|
||||
|
||||
expect(doc.css('a').length).to eq 1
|
||||
|
@ -69,8 +69,8 @@ describe Banzai::Filter::ReferenceRedactorFilter do
|
|||
it 'removes unpermitted references' do
|
||||
user = create(:user)
|
||||
project = create(:project)
|
||||
|
||||
link = reference_link(project: project.id, reference_type: 'test')
|
||||
|
||||
doc = filter(link, current_user: user)
|
||||
|
||||
expect(doc.css('a').length).to eq 0
|
||||
|
@ -90,8 +90,8 @@ describe Banzai::Filter::ReferenceRedactorFilter do
|
|||
non_member = create(:user)
|
||||
project = create(:project, :public)
|
||||
issue = create(:issue, :confidential, project: project)
|
||||
|
||||
link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue')
|
||||
|
||||
doc = filter(link, current_user: non_member)
|
||||
|
||||
expect(doc.css('a').length).to eq 0
|
||||
|
@ -124,8 +124,8 @@ describe Banzai::Filter::ReferenceRedactorFilter do
|
|||
assignee = create(:user)
|
||||
project = create(:project, :public)
|
||||
issue = create(:issue, :confidential, project: project, assignees: [assignee])
|
||||
|
||||
link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue')
|
||||
|
||||
doc = filter(link, current_user: assignee)
|
||||
|
||||
expect(doc.css('a').length).to eq 1
|
||||
|
@ -136,8 +136,8 @@ describe Banzai::Filter::ReferenceRedactorFilter do
|
|||
project = create(:project, :public)
|
||||
project.add_developer(member)
|
||||
issue = create(:issue, :confidential, project: project)
|
||||
|
||||
link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue')
|
||||
|
||||
doc = filter(link, current_user: member)
|
||||
|
||||
expect(doc.css('a').length).to eq 1
|
||||
|
@ -147,20 +147,62 @@ describe Banzai::Filter::ReferenceRedactorFilter do
|
|||
admin = create(:admin)
|
||||
project = create(:project, :public)
|
||||
issue = create(:issue, :confidential, project: project)
|
||||
|
||||
link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue')
|
||||
|
||||
doc = filter(link, current_user: admin)
|
||||
|
||||
expect(doc.css('a').length).to eq 1
|
||||
end
|
||||
|
||||
context "when a confidential issue is moved from a public project to a private one" do
|
||||
let(:public_project) { create(:project, :public) }
|
||||
let(:private_project) { create(:project, :private) }
|
||||
|
||||
it 'removes references for author' do
|
||||
author = create(:user)
|
||||
issue = create(:issue, :confidential, project: public_project, author: author)
|
||||
issue.update!(project: private_project) # move issue to private project
|
||||
link = reference_link(project: private_project.id, issue: issue.id, reference_type: 'issue')
|
||||
|
||||
doc = filter(link, current_user: author)
|
||||
|
||||
expect(doc.css('a').length).to eq 0
|
||||
end
|
||||
|
||||
it 'removes references for assignee' do
|
||||
assignee = create(:user)
|
||||
issue = create(:issue, :confidential, project: public_project, assignees: [assignee])
|
||||
issue.update!(project: private_project) # move issue to private project
|
||||
link = reference_link(project: private_project.id, issue: issue.id, reference_type: 'issue')
|
||||
|
||||
doc = filter(link, current_user: assignee)
|
||||
|
||||
expect(doc.css('a').length).to eq 0
|
||||
end
|
||||
|
||||
it 'allows references for project members' do
|
||||
member = create(:user)
|
||||
project = create(:project, :public)
|
||||
project_2 = create(:project, :private)
|
||||
project.add_developer(member)
|
||||
project_2.add_developer(member)
|
||||
issue = create(:issue, :confidential, project: project)
|
||||
issue.update!(project: project_2) # move issue to private project
|
||||
link = reference_link(project: project_2.id, issue: issue.id, reference_type: 'issue')
|
||||
|
||||
doc = filter(link, current_user: member)
|
||||
|
||||
expect(doc.css('a').length).to eq 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'allows references for non confidential issues' do
|
||||
user = create(:user)
|
||||
project = create(:project, :public)
|
||||
issue = create(:issue, project: project)
|
||||
|
||||
link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue')
|
||||
|
||||
doc = filter(link, current_user: user)
|
||||
|
||||
expect(doc.css('a').length).to eq 1
|
||||
|
@ -172,8 +214,8 @@ describe Banzai::Filter::ReferenceRedactorFilter do
|
|||
it 'removes unpermitted Group references' do
|
||||
user = create(:user)
|
||||
group = create(:group, :private)
|
||||
|
||||
link = reference_link(group: group.id, reference_type: 'user')
|
||||
|
||||
doc = filter(link, current_user: user)
|
||||
|
||||
expect(doc.css('a').length).to eq 0
|
||||
|
@ -183,8 +225,8 @@ describe Banzai::Filter::ReferenceRedactorFilter do
|
|||
user = create(:user)
|
||||
group = create(:group, :private)
|
||||
group.add_developer(user)
|
||||
|
||||
link = reference_link(group: group.id, reference_type: 'user')
|
||||
|
||||
doc = filter(link, current_user: user)
|
||||
|
||||
expect(doc.css('a').length).to eq 1
|
||||
|
@ -200,8 +242,8 @@ describe Banzai::Filter::ReferenceRedactorFilter do
|
|||
context 'with data-user' do
|
||||
it 'allows any User reference' do
|
||||
user = create(:user)
|
||||
|
||||
link = reference_link(user: user.id, reference_type: 'user')
|
||||
|
||||
doc = filter(link)
|
||||
|
||||
expect(doc.css('a').length).to eq 1
|
||||
|
|
|
@ -164,6 +164,12 @@ describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
|
|||
|
||||
expect(subject).to eq(Gitlab::Auth::Result.new(build.user, build.project, :build, described_class.build_authentication_abilities))
|
||||
end
|
||||
|
||||
it 'fails with blocked user token' do
|
||||
build.update(user: create(:user, :blocked))
|
||||
|
||||
expect(subject).to eq(Gitlab::Auth::Result.new(nil, nil, nil, nil))
|
||||
end
|
||||
end
|
||||
|
||||
(HasStatus::AVAILABLE_STATUSES - ['running']).each do |build_status|
|
||||
|
@ -259,6 +265,15 @@ describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
|
|||
|
||||
gl_auth.find_for_git_client("oauth2", token_w_api_scope.token, project: nil, ip: 'ip')
|
||||
end
|
||||
|
||||
context 'blocked user' do
|
||||
let(:user) { create(:user, :blocked) }
|
||||
|
||||
it 'fails' do
|
||||
expect(gl_auth.find_for_git_client("oauth2", token_w_api_scope.token, project: nil, ip: 'ip'))
|
||||
.to eq(Gitlab::Auth::Result.new(nil, nil, nil, nil))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'while using personal access tokens as passwords' do
|
||||
|
@ -307,9 +322,35 @@ describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
|
|||
it 'fails if password is nil' do
|
||||
expect_results_with_abilities(nil, nil, false)
|
||||
end
|
||||
|
||||
context 'when user is blocked' do
|
||||
let(:user) { create(:user, :blocked) }
|
||||
let(:personal_access_token) { create(:personal_access_token, scopes: ['read_registry'], user: user) }
|
||||
|
||||
before do
|
||||
stub_container_registry_config(enabled: true)
|
||||
end
|
||||
|
||||
it 'fails if user is blocked' do
|
||||
expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip'))
|
||||
.to eq(Gitlab::Auth::Result.new(nil, nil, nil, nil))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'while using regular user and password' do
|
||||
it 'fails for a blocked user' do
|
||||
user = create(
|
||||
:user,
|
||||
:blocked,
|
||||
username: 'normal_user',
|
||||
password: 'my-secret'
|
||||
)
|
||||
|
||||
expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip'))
|
||||
.to eq(Gitlab::Auth::Result.new(nil, nil, nil, nil))
|
||||
end
|
||||
|
||||
it 'goes through lfs authentication' do
|
||||
user = create(
|
||||
:user,
|
||||
|
|
|
@ -68,6 +68,16 @@ describe Gitlab::Gfm::UploadsRewriter do
|
|||
expect(moved_text.scan(/\A\[.*?\]/).count).to eq(1)
|
||||
end
|
||||
|
||||
context 'path traversal in file name' do
|
||||
let(:text) do
|
||||
"![a](/uploads/11111111111111111111111111111111/../../../../../../../../../../../../../../etc/passwd)"
|
||||
end
|
||||
|
||||
it 'throw an error' do
|
||||
expect { rewriter.rewrite(new_project) }.to raise_error(an_instance_of(StandardError).and having_attributes(message: "Invalid path"))
|
||||
end
|
||||
end
|
||||
|
||||
context "file are stored locally" do
|
||||
include_examples "files are accessible"
|
||||
end
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe Gitlab::HotlinkingDetector do
|
||||
describe ".intercept_hotlinking?" do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
subject { described_class.intercept_hotlinking?(request) }
|
||||
|
||||
let(:request) { double("request", headers: headers) }
|
||||
let(:headers) { {} }
|
||||
|
||||
context "hotlinked as media" do
|
||||
where(:return_value, :accept_header) do
|
||||
# These are default formats in modern browsers, and IE
|
||||
false | "*/*"
|
||||
false | "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
|
||||
false | "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"
|
||||
false | "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
|
||||
false | "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8"
|
||||
false | "image/jpeg, application/x-ms-application, image/gif, application/xaml+xml, image/pjpeg, application/x-ms-xbap, application/x-shockwave-flash, application/msword, */*"
|
||||
false | "text/html, application/xhtml+xml, image/jxr, */*"
|
||||
false | "text/html, application/xml;q=0.9, application/xhtml+xml, image/png, image/webp, image/jpeg, image/gif, image/x-xbitmap, */*;q=0.1"
|
||||
|
||||
# These are image request formats
|
||||
true | "image/webp,*/*"
|
||||
true | "image/png,image/*;q=0.8,*/*;q=0.5"
|
||||
true | "image/webp,image/apng,image/*,*/*;q=0.8"
|
||||
true | "image/png,image/svg+xml,image/*;q=0.8, */*;q=0.5"
|
||||
|
||||
# Video request formats
|
||||
true | "video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5"
|
||||
|
||||
# Audio request formats
|
||||
true | "audio/webm,audio/ogg,audio/wav,audio/*;q=0.9,application/ogg;q=0.7,video/*;q=0.6,*/*;q=0.5"
|
||||
|
||||
# CSS request formats
|
||||
true | "text/css,*/*;q=0.1"
|
||||
true | "text/css"
|
||||
true | "text/css,*/*;q=0.1"
|
||||
end
|
||||
|
||||
with_them do
|
||||
let(:headers) do
|
||||
{ "Accept" => accept_header }
|
||||
end
|
||||
|
||||
it { is_expected.to be(return_value) }
|
||||
end
|
||||
end
|
||||
|
||||
context "hotlinked as a script" do
|
||||
where(:return_value, :fetch_mode) do
|
||||
# Standard navigation fetch modes
|
||||
false | "navigate"
|
||||
false | "nested-navigate"
|
||||
false | "same-origin"
|
||||
|
||||
# Fetch modes when linking as JS
|
||||
true | "cors"
|
||||
true | "no-cors"
|
||||
true | "websocket"
|
||||
end
|
||||
|
||||
with_them do
|
||||
let(:headers) do
|
||||
{ "Sec-Fetch-Mode" => fetch_mode }
|
||||
end
|
||||
|
||||
it { is_expected.to be(return_value) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -32,6 +32,9 @@ describe Gitlab::ImportExport::AttributeCleaner do
|
|||
'issue_ids' => [1, 2, 3],
|
||||
'merge_request_ids' => [1, 2, 3],
|
||||
'note_ids' => [1, 2, 3],
|
||||
'remote_attachment_url' => 'http://something.dodgy',
|
||||
'remote_attachment_request_header' => 'bad value',
|
||||
'remote_attachment_urls' => %w(http://something.dodgy http://something.okay),
|
||||
'attributes' => {
|
||||
'issue_ids' => [1, 2, 3],
|
||||
'merge_request_ids' => [1, 2, 3],
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_relative 'shared_example.rb'
|
||||
|
||||
describe Gitlab::ImportExport::JSON::LegacyReader::File do
|
||||
it_behaves_like 'import/export json legacy reader' do
|
||||
let(:valid_path) { 'spec/fixtures/lib/gitlab/import_export/light/project.json' }
|
||||
let(:data) { valid_path }
|
||||
let(:json_data) { JSON.parse(File.read(valid_path)) }
|
||||
end
|
||||
|
||||
describe '#exist?' do
|
||||
let(:legacy_reader) do
|
||||
described_class.new(path, relation_names: [])
|
||||
end
|
||||
|
||||
subject { legacy_reader.exist? }
|
||||
|
||||
context 'given valid path' do
|
||||
let(:path) { 'spec/fixtures/lib/gitlab/import_export/light/project.json' }
|
||||
|
||||
it { is_expected.to be true }
|
||||
end
|
||||
|
||||
context 'given invalid path' do
|
||||
let(:path) { 'spec/non-existing-folder/do-not-create-this-file.json' }
|
||||
|
||||
it { is_expected.to be false }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,35 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_relative 'shared_example.rb'
|
||||
|
||||
describe Gitlab::ImportExport::JSON::LegacyReader::Hash do
|
||||
it_behaves_like 'import/export json legacy reader' do
|
||||
let(:path) { 'spec/fixtures/lib/gitlab/import_export/light/project.json' }
|
||||
|
||||
# the hash is modified by the `LegacyReader`
|
||||
# we need to deep-dup it
|
||||
let(:json_data) { JSON.parse(File.read(path)) }
|
||||
let(:data) { JSON.parse(File.read(path)) }
|
||||
end
|
||||
|
||||
describe '#exist?' do
|
||||
let(:legacy_reader) do
|
||||
described_class.new(tree_hash, relation_names: [])
|
||||
end
|
||||
|
||||
subject { legacy_reader.exist? }
|
||||
|
||||
context 'tree_hash is nil' do
|
||||
let(:tree_hash) { nil }
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
context 'tree_hash presents' do
|
||||
let(:tree_hash) { { "issues": [] } }
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,110 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples 'import/export json legacy reader' do
|
||||
let(:relation_names) { [] }
|
||||
|
||||
let(:legacy_reader) do
|
||||
described_class.new(
|
||||
data,
|
||||
relation_names: relation_names,
|
||||
allowed_path: "project")
|
||||
end
|
||||
|
||||
describe '#consume_attributes' do
|
||||
context 'when valid path is passed' do
|
||||
subject { legacy_reader.consume_attributes("project") }
|
||||
|
||||
context 'no excluded attributes' do
|
||||
let(:excluded_attributes) { [] }
|
||||
let(:relation_names) { [] }
|
||||
|
||||
it 'returns the whole tree from parsed JSON' do
|
||||
expect(subject).to eq(json_data)
|
||||
end
|
||||
end
|
||||
|
||||
context 'some attributes are excluded' do
|
||||
let(:relation_names) { %w[milestones labels] }
|
||||
|
||||
it 'returns hash without excluded attributes and relations' do
|
||||
expect(subject).not_to include('milestones', 'labels')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when invalid path is passed' do
|
||||
it 'raises an exception' do
|
||||
expect { legacy_reader.consume_attributes("invalid-path") }
|
||||
.to raise_error(ArgumentError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#consume_relation' do
|
||||
context 'when valid path is passed' do
|
||||
let(:key) { 'description' }
|
||||
|
||||
context 'block not given' do
|
||||
it 'returns value of the key' do
|
||||
expect(legacy_reader).to receive(:relations).and_return({ key => 'test value' })
|
||||
expect(legacy_reader.consume_relation("project", key)).to eq('test value')
|
||||
end
|
||||
end
|
||||
|
||||
context 'key has been consumed' do
|
||||
before do
|
||||
legacy_reader.consume_relation("project", key)
|
||||
end
|
||||
|
||||
it 'does not yield' do
|
||||
expect do |blk|
|
||||
legacy_reader.consume_relation("project", key, &blk)
|
||||
end.not_to yield_control
|
||||
end
|
||||
end
|
||||
|
||||
context 'value is nil' do
|
||||
before do
|
||||
expect(legacy_reader).to receive(:relations).and_return({ key => nil })
|
||||
end
|
||||
|
||||
it 'does not yield' do
|
||||
expect do |blk|
|
||||
legacy_reader.consume_relation("project", key, &blk)
|
||||
end.not_to yield_control
|
||||
end
|
||||
end
|
||||
|
||||
context 'value is not array' do
|
||||
before do
|
||||
expect(legacy_reader).to receive(:relations).and_return({ key => 'value' })
|
||||
end
|
||||
|
||||
it 'yield the value with index 0' do
|
||||
expect do |blk|
|
||||
legacy_reader.consume_relation("project", key, &blk)
|
||||
end.to yield_with_args('value', 0)
|
||||
end
|
||||
end
|
||||
|
||||
context 'value is an array' do
|
||||
before do
|
||||
expect(legacy_reader).to receive(:relations).and_return({ key => %w[item1 item2 item3] })
|
||||
end
|
||||
|
||||
it 'yield each array element with index' do
|
||||
expect do |blk|
|
||||
legacy_reader.consume_relation("project", key, &blk)
|
||||
end.to yield_successive_args(['item1', 0], ['item2', 1], ['item3', 2])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when invalid path is passed' do
|
||||
it 'raises an exception' do
|
||||
expect { legacy_reader.consume_relation("invalid") }
|
||||
.to raise_error(ArgumentError)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,149 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::ImportExport::JSON::LegacyReader::User do
|
||||
let(:relation_names) { [] }
|
||||
let(:legacy_reader) { described_class.new(tree_hash, relation_names) }
|
||||
|
||||
describe '#valid?' do
|
||||
subject { legacy_reader.valid? }
|
||||
|
||||
context 'tree_hash not present' do
|
||||
let(:tree_hash) { nil }
|
||||
|
||||
it { is_expected.to be false }
|
||||
end
|
||||
|
||||
context 'tree_hash presents' do
|
||||
let(:tree_hash) { { "issues": [] } }
|
||||
|
||||
it { is_expected.to be true }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe Gitlab::ImportExport::JSON::LegacyReader::File do
|
||||
let(:fixture) { 'spec/fixtures/lib/gitlab/import_export/light/project.json' }
|
||||
let(:project_tree) { JSON.parse(File.read(fixture)) }
|
||||
let(:relation_names) { [] }
|
||||
let(:legacy_reader) { described_class.new(path, relation_names) }
|
||||
|
||||
describe '#valid?' do
|
||||
subject { legacy_reader.valid? }
|
||||
|
||||
context 'given valid path' do
|
||||
let(:path) { fixture }
|
||||
|
||||
it { is_expected.to be true }
|
||||
end
|
||||
|
||||
context 'given invalid path' do
|
||||
let(:path) { 'spec/non-existing-folder/do-not-create-this-file.json' }
|
||||
|
||||
it { is_expected.to be false }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#root_attributes' do
|
||||
let(:path) { fixture }
|
||||
|
||||
subject { legacy_reader.root_attributes(excluded_attributes) }
|
||||
|
||||
context 'No excluded attributes' do
|
||||
let(:excluded_attributes) { [] }
|
||||
let(:relation_names) { [] }
|
||||
|
||||
it 'returns the whole tree from parsed JSON' do
|
||||
expect(subject).to eq(project_tree)
|
||||
end
|
||||
end
|
||||
|
||||
context 'Some attributes are excluded' do
|
||||
let(:excluded_attributes) { %w[milestones labels issues services snippets] }
|
||||
let(:relation_names) { %w[import_type archived] }
|
||||
|
||||
it 'returns hash without excluded attributes and relations' do
|
||||
expect(subject).not_to include('milestones', 'labels', 'issues', 'services', 'snippets', 'import_type', 'archived')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#consume_relation' do
|
||||
let(:path) { fixture }
|
||||
let(:key) { 'description' }
|
||||
|
||||
context 'block not given' do
|
||||
it 'returns value of the key' do
|
||||
expect(legacy_reader).to receive(:relations).and_return({ key => 'test value' })
|
||||
expect(legacy_reader.consume_relation(key)).to eq('test value')
|
||||
end
|
||||
end
|
||||
|
||||
context 'key has been consumed' do
|
||||
before do
|
||||
legacy_reader.consume_relation(key)
|
||||
end
|
||||
|
||||
it 'does not yield' do
|
||||
expect do |blk|
|
||||
legacy_reader.consume_relation(key, &blk)
|
||||
end.not_to yield_control
|
||||
end
|
||||
end
|
||||
|
||||
context 'value is nil' do
|
||||
before do
|
||||
expect(legacy_reader).to receive(:relations).and_return({ key => nil })
|
||||
end
|
||||
|
||||
it 'does not yield' do
|
||||
expect do |blk|
|
||||
legacy_reader.consume_relation(key, &blk)
|
||||
end.not_to yield_control
|
||||
end
|
||||
end
|
||||
|
||||
context 'value is not array' do
|
||||
before do
|
||||
expect(legacy_reader).to receive(:relations).and_return({ key => 'value' })
|
||||
end
|
||||
|
||||
it 'yield the value with index 0' do
|
||||
expect do |blk|
|
||||
legacy_reader.consume_relation(key, &blk)
|
||||
end.to yield_with_args('value', 0)
|
||||
end
|
||||
end
|
||||
|
||||
context 'value is an array' do
|
||||
before do
|
||||
expect(legacy_reader).to receive(:relations).and_return({ key => %w[item1 item2 item3] })
|
||||
end
|
||||
|
||||
it 'yield each array element with index' do
|
||||
expect do |blk|
|
||||
legacy_reader.consume_relation(key, &blk)
|
||||
end.to yield_successive_args(['item1', 0], ['item2', 1], ['item3', 2])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#tree_hash' do
|
||||
let(:path) { fixture }
|
||||
|
||||
subject { legacy_reader.send(:tree_hash) }
|
||||
|
||||
it 'parses the JSON into the expected tree' do
|
||||
expect(subject).to eq(project_tree)
|
||||
end
|
||||
|
||||
context 'invalid JSON' do
|
||||
let(:path) { 'spec/fixtures/lib/gitlab/import_export/invalid_json/project.json' }
|
||||
|
||||
it 'raise Exception' do
|
||||
expect { subject }.to raise_exception(Gitlab::ImportExport::Error, 'Incorrect JSON format')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -5,20 +5,37 @@ require 'spec_helper'
|
|||
describe Gitlab::ImportExport::JSON::LegacyWriter do
|
||||
let(:path) { "#{Dir.tmpdir}/legacy_writer_spec/test.json" }
|
||||
|
||||
subject { described_class.new(path) }
|
||||
subject do
|
||||
described_class.new(path, allowed_path: "project")
|
||||
end
|
||||
|
||||
after do
|
||||
FileUtils.rm_rf(path)
|
||||
end
|
||||
|
||||
describe "#write" do
|
||||
describe "#write_attributes" do
|
||||
it "writes correct json" do
|
||||
expected_hash = { "key" => "value_1", "key_1" => "value_2" }
|
||||
subject.write_attributes("project", expected_hash)
|
||||
|
||||
expect(subject_json).to eq(expected_hash)
|
||||
end
|
||||
|
||||
context 'when invalid path is used' do
|
||||
it 'raises an exception' do
|
||||
expect { subject.write_attributes("invalid", { "key" => "value" }) }
|
||||
.to raise_error(ArgumentError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#write_relation" do
|
||||
context "when key is already written" do
|
||||
it "raises exception" do
|
||||
key = "key"
|
||||
value = "value"
|
||||
subject.write(key, value)
|
||||
subject.write_relation("project", "key", "old value")
|
||||
|
||||
expect { subject.write(key, "new value") }.to raise_exception("key '#{key}' already written")
|
||||
expect { subject.write_relation("project", "key", "new value") }
|
||||
.to raise_exception("key 'key' already written")
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -27,53 +44,58 @@ describe Gitlab::ImportExport::JSON::LegacyWriter do
|
|||
it "writes correct json" do
|
||||
expected_hash = { "key" => "value_1", "key_1" => "value_2" }
|
||||
expected_hash.each do |key, value|
|
||||
subject.write(key, value)
|
||||
subject.write_relation("project", key, value)
|
||||
end
|
||||
subject.close
|
||||
|
||||
expect(saved_json(path)).to eq(expected_hash)
|
||||
expect(subject_json).to eq(expected_hash)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when invalid path is used' do
|
||||
it 'raises an exception' do
|
||||
expect { subject.write_relation("invalid", "key", "value") }
|
||||
.to raise_error(ArgumentError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#append" do
|
||||
describe "#write_relation_array" do
|
||||
context 'when array is used' do
|
||||
it 'writes correct json' do
|
||||
subject.write_relation_array("project", "key", ["value"])
|
||||
|
||||
expect(subject_json).to eq({ "key" => ["value"] })
|
||||
end
|
||||
end
|
||||
|
||||
context 'when enumerable is used' do
|
||||
it 'writes correct json' do
|
||||
values = %w(value1 value2)
|
||||
|
||||
enumerator = Enumerator.new do |items|
|
||||
values.each { |value| items << value }
|
||||
end
|
||||
|
||||
subject.write_relation_array("project", "key", enumerator)
|
||||
|
||||
expect(subject_json).to eq({ "key" => values })
|
||||
end
|
||||
end
|
||||
|
||||
context "when key is already written" do
|
||||
it "appends values under a given key" do
|
||||
key = "key"
|
||||
values = %w(value_1 value_2)
|
||||
expected_hash = { key => values }
|
||||
values.each do |value|
|
||||
subject.append(key, value)
|
||||
end
|
||||
subject.close
|
||||
it "raises an exception" do
|
||||
subject.write_relation_array("project", "key", %w(old_value))
|
||||
|
||||
expect(saved_json(path)).to eq(expected_hash)
|
||||
end
|
||||
end
|
||||
|
||||
context "when key is not already written" do
|
||||
it "writes correct json" do
|
||||
expected_hash = { "key" => ["value"] }
|
||||
subject.append("key", "value")
|
||||
subject.close
|
||||
|
||||
expect(saved_json(path)).to eq(expected_hash)
|
||||
expect { subject.write_relation_array("project", "key", %w(new_value)) }
|
||||
.to raise_error(ArgumentError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#set" do
|
||||
it "writes correct json" do
|
||||
expected_hash = { "key" => "value_1", "key_1" => "value_2" }
|
||||
subject.set(expected_hash)
|
||||
subject.close
|
||||
def subject_json
|
||||
subject.close
|
||||
|
||||
expect(saved_json(path)).to eq(expected_hash)
|
||||
end
|
||||
end
|
||||
|
||||
def saved_json(filename)
|
||||
::JSON.parse(IO.read(filename))
|
||||
::JSON.parse(IO.read(subject.path))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,23 +14,24 @@ describe Gitlab::ImportExport::RelationTreeRestorer do
|
|||
|
||||
let(:user) { create(:user) }
|
||||
let(:shared) { Gitlab::ImportExport::Shared.new(importable) }
|
||||
let(:members_mapper) { Gitlab::ImportExport::MembersMapper.new(exported_members: {}, user: user, importable: importable) }
|
||||
let(:attributes) { {} }
|
||||
|
||||
let(:importable_hash) do
|
||||
json = IO.read(path)
|
||||
ActiveSupport::JSON.decode(json)
|
||||
let(:members_mapper) do
|
||||
Gitlab::ImportExport::MembersMapper.new(exported_members: {}, user: user, importable: importable)
|
||||
end
|
||||
|
||||
let(:relation_tree_restorer) do
|
||||
described_class.new(
|
||||
user: user,
|
||||
shared: shared,
|
||||
relation_reader: relation_reader,
|
||||
importable: importable,
|
||||
object_builder: object_builder,
|
||||
members_mapper: members_mapper,
|
||||
relation_factory: relation_factory,
|
||||
reader: reader
|
||||
user: user,
|
||||
shared: shared,
|
||||
relation_reader: relation_reader,
|
||||
object_builder: object_builder,
|
||||
members_mapper: members_mapper,
|
||||
relation_factory: relation_factory,
|
||||
reader: reader,
|
||||
importable: importable,
|
||||
importable_path: nil,
|
||||
importable_attributes: attributes
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -100,7 +101,14 @@ describe Gitlab::ImportExport::RelationTreeRestorer do
|
|||
let(:reader) { Gitlab::ImportExport::Reader.new(shared: shared) }
|
||||
|
||||
context 'using legacy reader' do
|
||||
let(:relation_reader) { Gitlab::ImportExport::JSON::LegacyReader::File.new(path, reader.project_relation_names) }
|
||||
let(:relation_reader) do
|
||||
Gitlab::ImportExport::JSON::LegacyReader::File.new(
|
||||
path,
|
||||
relation_names: reader.project_relation_names
|
||||
)
|
||||
end
|
||||
|
||||
let(:attributes) { relation_reader.consume_attributes(nil) }
|
||||
|
||||
it_behaves_like 'import project successfully'
|
||||
|
||||
|
@ -119,7 +127,7 @@ describe Gitlab::ImportExport::RelationTreeRestorer do
|
|||
let(:importable) { create(:group, parent: group) }
|
||||
let(:object_builder) { Gitlab::ImportExport::Group::ObjectBuilder }
|
||||
let(:relation_factory) { Gitlab::ImportExport::Group::RelationFactory }
|
||||
let(:relation_reader) { Gitlab::ImportExport::JSON::LegacyReader::File.new(path, reader.group_relation_names) }
|
||||
let(:relation_reader) { Gitlab::ImportExport::JSON::LegacyReader::File.new(path, relation_names: reader.group_relation_names) }
|
||||
let(:reader) do
|
||||
Gitlab::ImportExport::Reader.new(
|
||||
shared: shared,
|
||||
|
|
|
@ -3,9 +3,7 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::Regex do
|
||||
describe '.project_name_regex' do
|
||||
subject { described_class.project_name_regex }
|
||||
|
||||
shared_examples_for 'project/group name regex' do
|
||||
it { is_expected.to match('gitlab-ce') }
|
||||
it { is_expected.to match('GitLab CE') }
|
||||
it { is_expected.to match('100 lines') }
|
||||
|
@ -15,6 +13,34 @@ describe Gitlab::Regex do
|
|||
it { is_expected.not_to match('?gitlab') }
|
||||
end
|
||||
|
||||
shared_examples_for 'project/group name error message' do
|
||||
it { is_expected.to eq("can contain only letters, digits, emojis, '_', '.', dash, space. It must start with letter, digit, emoji or '_'.") }
|
||||
end
|
||||
|
||||
describe '.project_name_regex' do
|
||||
subject { described_class.project_name_regex }
|
||||
|
||||
it_behaves_like 'project/group name regex'
|
||||
end
|
||||
|
||||
describe '.group_name_regex' do
|
||||
subject { described_class.group_name_regex }
|
||||
|
||||
it_behaves_like 'project/group name regex'
|
||||
end
|
||||
|
||||
describe '.project_name_regex_message' do
|
||||
subject { described_class.project_name_regex_message }
|
||||
|
||||
it_behaves_like 'project/group name error message'
|
||||
end
|
||||
|
||||
describe '.group_name_regex_message' do
|
||||
subject { described_class.group_name_regex_message }
|
||||
|
||||
it_behaves_like 'project/group name error message'
|
||||
end
|
||||
|
||||
describe '.environment_name_regex' do
|
||||
subject { described_class.environment_name_regex }
|
||||
|
||||
|
|
|
@ -13,9 +13,9 @@ describe Notify do
|
|||
|
||||
let(:current_user_sanitized) { 'www_example_com' }
|
||||
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:current_user) { create(:user, email: "current@email.com", name: 'www.example.com') }
|
||||
let_it_be(:assignee) { create(:user, email: 'assignee@example.com', name: 'John Doe') }
|
||||
let_it_be(:user, reload: true) { create(:user) }
|
||||
let_it_be(:current_user, reload: true) { create(:user, email: "current@email.com", name: 'www.example.com') }
|
||||
let_it_be(:assignee, reload: true) { create(:user, email: 'assignee@example.com', name: 'John Doe') }
|
||||
|
||||
let_it_be(:merge_request) do
|
||||
create(:merge_request, source_project: project,
|
||||
|
|
|
@ -48,6 +48,9 @@ describe Group do
|
|||
|
||||
describe 'validations' do
|
||||
it { is_expected.to validate_presence_of :name }
|
||||
it { is_expected.to allow_value('group test_4').for(:name) }
|
||||
it { is_expected.not_to allow_value('test/../foo').for(:name) }
|
||||
it { is_expected.not_to allow_value('<script>alert("Attack!")</script>').for(:name) }
|
||||
it { is_expected.to validate_presence_of :path }
|
||||
it { is_expected.not_to validate_presence_of :owner }
|
||||
it { is_expected.to validate_presence_of :two_factor_grace_period }
|
||||
|
|
|
@ -529,88 +529,146 @@ describe Issue do
|
|||
end
|
||||
|
||||
describe '#visible_to_user?' do
|
||||
let(:project) { build(:project) }
|
||||
let(:issue) { build(:issue, project: project) }
|
||||
let(:user) { create(:user) }
|
||||
|
||||
subject { issue.visible_to_user?(user) }
|
||||
|
||||
context 'with a project' do
|
||||
it 'returns false when feature is disabled' do
|
||||
project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
|
||||
|
||||
is_expected.to eq(false)
|
||||
end
|
||||
|
||||
it 'returns false when restricted for members' do
|
||||
project.project_feature.update_attribute(:issues_access_level, ProjectFeature::PRIVATE)
|
||||
|
||||
is_expected.to eq(false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'without a user' do
|
||||
let(:issue) { build(:issue) }
|
||||
let(:user) { nil }
|
||||
|
||||
it 'returns true when the issue is publicly visible' do
|
||||
expect(issue).to receive(:publicly_visible?).and_return(true)
|
||||
|
||||
expect(issue.visible_to_user?).to eq(true)
|
||||
is_expected.to eq(true)
|
||||
end
|
||||
|
||||
it 'returns false when the issue is not publicly visible' do
|
||||
expect(issue).to receive(:publicly_visible?).and_return(false)
|
||||
|
||||
expect(issue.visible_to_user?).to eq(false)
|
||||
is_expected.to eq(false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a user' do
|
||||
let(:user) { create(:user) }
|
||||
let(:issue) { build(:issue) }
|
||||
|
||||
it 'returns true when the issue is readable' do
|
||||
expect(issue).to receive(:readable_by?).with(user).and_return(true)
|
||||
|
||||
expect(issue.visible_to_user?(user)).to eq(true)
|
||||
shared_examples 'issue readable by user' do
|
||||
it { is_expected.to eq(true) }
|
||||
end
|
||||
|
||||
it 'returns false when the issue is not readable' do
|
||||
expect(issue).to receive(:readable_by?).with(user).and_return(false)
|
||||
|
||||
expect(issue.visible_to_user?(user)).to eq(false)
|
||||
shared_examples 'issue not readable by user' do
|
||||
it { is_expected.to eq(false) }
|
||||
end
|
||||
|
||||
it 'returns false when feature is disabled' do
|
||||
expect(issue).not_to receive(:readable_by?)
|
||||
shared_examples 'confidential issue readable by user' do
|
||||
specify do
|
||||
issue.confidential = true
|
||||
|
||||
issue.project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
|
||||
|
||||
expect(issue.visible_to_user?(user)).to eq(false)
|
||||
end
|
||||
|
||||
it 'returns false when restricted for members' do
|
||||
expect(issue).not_to receive(:readable_by?)
|
||||
|
||||
issue.project.project_feature.update_attribute(:issues_access_level, ProjectFeature::PRIVATE)
|
||||
|
||||
expect(issue.visible_to_user?(user)).to eq(false)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with a regular user that is not a team member' do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
context 'using a public project' do
|
||||
let(:project) { create(:project, :public) }
|
||||
|
||||
it 'returns true for a regular issue' do
|
||||
issue = build(:issue, project: project)
|
||||
|
||||
expect(issue.visible_to_user?(user)).to eq(true)
|
||||
end
|
||||
|
||||
it 'returns false for a confidential issue' do
|
||||
issue = build(:issue, project: project, confidential: true)
|
||||
|
||||
expect(issue.visible_to_user?(user)).to eq(false)
|
||||
is_expected.to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'using an internal project' do
|
||||
let(:project) { create(:project, :internal) }
|
||||
shared_examples 'confidential issue not readable by user' do
|
||||
specify do
|
||||
issue.confidential = true
|
||||
|
||||
context 'using an internal user' do
|
||||
it 'returns true for a regular issue' do
|
||||
issue = build(:issue, project: project)
|
||||
is_expected.to eq(false)
|
||||
end
|
||||
end
|
||||
|
||||
expect(issue.visible_to_user?(user)).to eq(true)
|
||||
context 'with an admin user' do
|
||||
let(:user) { build(:admin) }
|
||||
|
||||
it_behaves_like 'issue readable by user'
|
||||
it_behaves_like 'confidential issue readable by user'
|
||||
end
|
||||
|
||||
context 'with an owner' do
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
end
|
||||
|
||||
it_behaves_like 'issue readable by user'
|
||||
it_behaves_like 'confidential issue readable by user'
|
||||
end
|
||||
|
||||
context 'with a reporter user' do
|
||||
before do
|
||||
project.add_reporter(user)
|
||||
end
|
||||
|
||||
it_behaves_like 'issue readable by user'
|
||||
it_behaves_like 'confidential issue readable by user'
|
||||
end
|
||||
|
||||
context 'with a guest user' do
|
||||
before do
|
||||
project.add_guest(user)
|
||||
end
|
||||
|
||||
it_behaves_like 'issue readable by user'
|
||||
it_behaves_like 'confidential issue not readable by user'
|
||||
|
||||
context 'when user is an assignee' do
|
||||
before do
|
||||
issue.update!(assignees: [user])
|
||||
end
|
||||
|
||||
it 'returns false for a confidential issue' do
|
||||
issue = build(:issue, :confidential, project: project)
|
||||
it_behaves_like 'issue readable by user'
|
||||
it_behaves_like 'confidential issue readable by user'
|
||||
end
|
||||
|
||||
expect(issue.visible_to_user?(user)).to eq(false)
|
||||
context 'when user is the author' do
|
||||
before do
|
||||
issue.update!(author: user)
|
||||
end
|
||||
|
||||
it_behaves_like 'issue readable by user'
|
||||
it_behaves_like 'confidential issue readable by user'
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a user that is not a member' do
|
||||
context 'using a public project' do
|
||||
let(:project) { build(:project, :public) }
|
||||
|
||||
it_behaves_like 'issue readable by user'
|
||||
it_behaves_like 'confidential issue not readable by user'
|
||||
end
|
||||
|
||||
context 'using an internal project' do
|
||||
let(:project) { build(:project, :internal) }
|
||||
|
||||
context 'using an internal user' do
|
||||
before do
|
||||
allow(user).to receive(:external?).and_return(false)
|
||||
end
|
||||
|
||||
it_behaves_like 'issue readable by user'
|
||||
it_behaves_like 'confidential issue not readable by user'
|
||||
end
|
||||
|
||||
context 'using an external user' do
|
||||
before do
|
||||
allow(user).to receive(:external?).and_return(true)
|
||||
end
|
||||
|
||||
it_behaves_like 'issue not readable by user'
|
||||
it_behaves_like 'confidential issue not readable by user'
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -619,134 +677,112 @@ describe Issue do
|
|||
allow(user).to receive(:external?).and_return(true)
|
||||
end
|
||||
|
||||
it 'returns false for a regular issue' do
|
||||
issue = build(:issue, project: project)
|
||||
|
||||
expect(issue.visible_to_user?(user)).to eq(false)
|
||||
end
|
||||
|
||||
it 'returns false for a confidential issue' do
|
||||
issue = build(:issue, :confidential, project: project)
|
||||
|
||||
expect(issue.visible_to_user?(user)).to eq(false)
|
||||
end
|
||||
it_behaves_like 'issue not readable by user'
|
||||
it_behaves_like 'confidential issue not readable by user'
|
||||
end
|
||||
end
|
||||
|
||||
context 'using a private project' do
|
||||
let(:project) { create(:project, :private) }
|
||||
context 'with an external authentication service' do
|
||||
before do
|
||||
enable_external_authorization_service_check
|
||||
end
|
||||
|
||||
it 'returns false for a regular issue' do
|
||||
it 'is `false` when an external authorization service is enabled' do
|
||||
issue = build(:issue, project: build(:project, :public))
|
||||
|
||||
expect(issue).not_to be_visible_to_user
|
||||
end
|
||||
|
||||
it 'checks the external service to determine if an issue is readable by a user' do
|
||||
project = build(:project, :public,
|
||||
external_authorization_classification_label: 'a-label')
|
||||
issue = build(:issue, project: project)
|
||||
user = build(:user)
|
||||
|
||||
expect(issue.visible_to_user?(user)).to eq(false)
|
||||
expect(::Gitlab::ExternalAuthorization).to receive(:access_allowed?).with(user, 'a-label') { false }
|
||||
expect(issue.visible_to_user?(user)).to be_falsy
|
||||
end
|
||||
|
||||
it 'returns false for a confidential issue' do
|
||||
issue = build(:issue, :confidential, project: project)
|
||||
it 'does not check the external service if a user does not have access to the project' do
|
||||
project = build(:project, :private,
|
||||
external_authorization_classification_label: 'a-label')
|
||||
issue = build(:issue, project: project)
|
||||
user = build(:user)
|
||||
|
||||
expect(issue.visible_to_user?(user)).to eq(false)
|
||||
expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
|
||||
expect(issue.visible_to_user?(user)).to be_falsy
|
||||
end
|
||||
|
||||
context 'when the user is the project owner' do
|
||||
it 'does not check the external webservice for admins' do
|
||||
issue = build(:issue)
|
||||
user = build(:admin)
|
||||
|
||||
expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
|
||||
|
||||
issue.visible_to_user?(user)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when issue is moved to a private project' do
|
||||
let(:private_project) { build(:project, :private)}
|
||||
|
||||
before do
|
||||
issue.update(project: private_project) # move issue to private project
|
||||
end
|
||||
|
||||
shared_examples 'issue visible if user has guest access' do
|
||||
context 'when user is not a member' do
|
||||
it_behaves_like 'issue not readable by user'
|
||||
it_behaves_like 'confidential issue not readable by user'
|
||||
end
|
||||
|
||||
context 'when user is a guest' do
|
||||
before do
|
||||
private_project.add_guest(user)
|
||||
end
|
||||
|
||||
it_behaves_like 'issue readable by user'
|
||||
it_behaves_like 'confidential issue readable by user'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is the author of the original issue' do
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
issue.update!(author: user)
|
||||
end
|
||||
|
||||
it 'returns true for a regular issue' do
|
||||
issue = build(:issue, project: project)
|
||||
it_behaves_like 'issue visible if user has guest access'
|
||||
end
|
||||
|
||||
expect(issue.visible_to_user?(user)).to eq(true)
|
||||
context 'when user is an assignee in the original issue' do
|
||||
before do
|
||||
issue.update!(assignees: [user])
|
||||
end
|
||||
|
||||
it 'returns true for a confidential issue' do
|
||||
issue = build(:issue, :confidential, project: project)
|
||||
it_behaves_like 'issue visible if user has guest access'
|
||||
end
|
||||
|
||||
expect(issue.visible_to_user?(user)).to eq(true)
|
||||
context 'when user is not the author or an assignee in original issue' do
|
||||
context 'when user is a guest' do
|
||||
before do
|
||||
private_project.add_guest(user)
|
||||
end
|
||||
|
||||
it_behaves_like 'issue readable by user'
|
||||
it_behaves_like 'confidential issue not readable by user'
|
||||
end
|
||||
|
||||
context 'when user is a reporter' do
|
||||
before do
|
||||
private_project.add_reporter(user)
|
||||
end
|
||||
|
||||
it_behaves_like 'issue readable by user'
|
||||
it_behaves_like 'confidential issue readable by user'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a regular user that is a team member' do
|
||||
let(:user) { create(:user) }
|
||||
let(:project) { create(:project, :public) }
|
||||
|
||||
context 'using a public project' do
|
||||
before do
|
||||
project.add_developer(user)
|
||||
end
|
||||
|
||||
it 'returns true for a regular issue' do
|
||||
issue = build(:issue, project: project)
|
||||
|
||||
expect(issue.visible_to_user?(user)).to eq(true)
|
||||
end
|
||||
|
||||
it 'returns true for a confidential issue' do
|
||||
issue = build(:issue, :confidential, project: project)
|
||||
|
||||
expect(issue.visible_to_user?(user)).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'using an internal project' do
|
||||
let(:project) { create(:project, :internal) }
|
||||
|
||||
before do
|
||||
project.add_developer(user)
|
||||
end
|
||||
|
||||
it 'returns true for a regular issue' do
|
||||
issue = build(:issue, project: project)
|
||||
|
||||
expect(issue.visible_to_user?(user)).to eq(true)
|
||||
end
|
||||
|
||||
it 'returns true for a confidential issue' do
|
||||
issue = build(:issue, :confidential, project: project)
|
||||
|
||||
expect(issue.visible_to_user?(user)).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'using a private project' do
|
||||
let(:project) { create(:project, :private) }
|
||||
|
||||
before do
|
||||
project.add_developer(user)
|
||||
end
|
||||
|
||||
it 'returns true for a regular issue' do
|
||||
issue = build(:issue, project: project)
|
||||
|
||||
expect(issue.visible_to_user?(user)).to eq(true)
|
||||
end
|
||||
|
||||
it 'returns true for a confidential issue' do
|
||||
issue = build(:issue, :confidential, project: project)
|
||||
|
||||
expect(issue.visible_to_user?(user)).to eq(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an admin user' do
|
||||
let(:project) { create(:project) }
|
||||
let(:user) { create(:admin) }
|
||||
|
||||
it 'returns true for a regular issue' do
|
||||
issue = build(:issue, project: project)
|
||||
|
||||
expect(issue.visible_to_user?(user)).to eq(true)
|
||||
end
|
||||
|
||||
it 'returns true for a confidential issue' do
|
||||
issue = build(:issue, :confidential, project: project)
|
||||
|
||||
expect(issue.visible_to_user?(user)).to eq(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#publicly_visible?' do
|
||||
|
@ -868,49 +904,6 @@ describe Issue do
|
|||
subject { create(:issue, updated_at: 1.hour.ago) }
|
||||
end
|
||||
|
||||
context 'when an external authentication service' do
|
||||
before do
|
||||
enable_external_authorization_service_check
|
||||
end
|
||||
|
||||
describe '#visible_to_user?' do
|
||||
it 'is `false` when an external authorization service is enabled' do
|
||||
issue = build(:issue, project: build(:project, :public))
|
||||
|
||||
expect(issue).not_to be_visible_to_user
|
||||
end
|
||||
|
||||
it 'checks the external service to determine if an issue is readable by a user' do
|
||||
project = build(:project, :public,
|
||||
external_authorization_classification_label: 'a-label')
|
||||
issue = build(:issue, project: project)
|
||||
user = build(:user)
|
||||
|
||||
expect(::Gitlab::ExternalAuthorization).to receive(:access_allowed?).with(user, 'a-label') { false }
|
||||
expect(issue.visible_to_user?(user)).to be_falsy
|
||||
end
|
||||
|
||||
it 'does not check the external service if a user does not have access to the project' do
|
||||
project = build(:project, :private,
|
||||
external_authorization_classification_label: 'a-label')
|
||||
issue = build(:issue, project: project)
|
||||
user = build(:user)
|
||||
|
||||
expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
|
||||
expect(issue.visible_to_user?(user)).to be_falsy
|
||||
end
|
||||
|
||||
it 'does not check the external webservice for admins' do
|
||||
issue = build(:issue)
|
||||
user = build(:admin)
|
||||
|
||||
expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
|
||||
|
||||
issue.visible_to_user?(user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#labels_hook_attrs" do
|
||||
let(:label) { create(:label) }
|
||||
let(:issue) { create(:labeled_issue, labels: [label]) }
|
||||
|
|
|
@ -103,12 +103,24 @@ describe IssuePolicy do
|
|||
expect(permissions(author, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue)
|
||||
end
|
||||
|
||||
it 'does not allow issue author to read or update confidential issue moved to an private project' do
|
||||
confidential_issue.project = build(:project, :private)
|
||||
|
||||
expect(permissions(author, confidential_issue)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue)
|
||||
end
|
||||
|
||||
it 'allows issue assignees to read and update their confidential issues' do
|
||||
expect(permissions(assignee, confidential_issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue)
|
||||
expect(permissions(assignee, confidential_issue)).to be_disallowed(:admin_issue)
|
||||
|
||||
expect(permissions(assignee, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue)
|
||||
end
|
||||
|
||||
it 'does not allow issue assignees to read or update confidential issue moved to an private project' do
|
||||
confidential_issue.project = build(:project, :private)
|
||||
|
||||
expect(permissions(assignee, confidential_issue)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -234,6 +234,25 @@ describe API::DeployTokens do
|
|||
expect(response).to match_response_schema('public_api/v4/deploy_token')
|
||||
end
|
||||
|
||||
context 'with no optional params given' do
|
||||
let(:params) do
|
||||
{
|
||||
name: 'Foo',
|
||||
scopes: [
|
||||
'read_repository'
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates the deploy token with default values' do
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:created)
|
||||
expect(json_response['username']).to match(/gitlab\+deploy-token-\d+/)
|
||||
expect(json_response['expires_at']).to eq(nil)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an invalid scope' do
|
||||
before do
|
||||
params[:scopes] = %w[read_repository all_access]
|
||||
|
|
|
@ -642,6 +642,20 @@ describe API::Groups do
|
|||
expect(json_response['default_branch_protection']).to eq(::Gitlab::Access::MAINTAINER_PROJECT_ACCESS)
|
||||
end
|
||||
|
||||
context 'malicious group name' do
|
||||
subject { put api("/groups/#{group1.id}", user1), params: { name: "<SCRIPT>alert('DOUBLE-ATTACK!')</SCRIPT>" } }
|
||||
|
||||
it 'returns bad request' do
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
end
|
||||
|
||||
it 'does not update group name' do
|
||||
expect { subject }.not_to change { group1.reload.name }
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns 404 for a non existing group' do
|
||||
put api('/groups/1328', user1), params: { name: new_group_name }
|
||||
|
||||
|
@ -1083,6 +1097,20 @@ describe API::Groups do
|
|||
expect(json_response["parent_id"]).to eq(parent.id)
|
||||
end
|
||||
|
||||
context 'malicious group name' do
|
||||
subject { post api("/groups", user3), params: group_params }
|
||||
|
||||
let(:group_params) { attributes_for_group_api name: "<SCRIPT>alert('ATTACKED!')</SCRIPT>", path: "unique-url" }
|
||||
|
||||
it 'returns bad request' do
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
end
|
||||
|
||||
it { expect { subject }.not_to change { Group.count } }
|
||||
end
|
||||
|
||||
it "does not create group, duplicate" do
|
||||
post api("/groups", user3), params: { name: 'Duplicate Test', path: group2.path }
|
||||
|
||||
|
|
|
@ -3,15 +3,14 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe API::Internal::Base do
|
||||
set(:user) { create(:user) }
|
||||
let_it_be(:user, reload: true) { create(:user) }
|
||||
let_it_be(:project, reload: true) { create(:project, :repository, :wiki_repo) }
|
||||
let_it_be(:personal_snippet) { create(:personal_snippet, :repository, author: user) }
|
||||
let_it_be(:project_snippet) { create(:project_snippet, :repository, author: user, project: project) }
|
||||
let(:key) { create(:key, user: user) }
|
||||
set(:project) { create(:project, :repository, :wiki_repo) }
|
||||
let(:secret_token) { Gitlab::Shell.secret_token }
|
||||
let(:gl_repository) { "project-#{project.id}" }
|
||||
let(:reference_counter) { double('ReferenceCounter') }
|
||||
|
||||
let_it_be(:personal_snippet) { create(:personal_snippet, :repository, author: user) }
|
||||
let_it_be(:project_snippet) { create(:project_snippet, :repository, author: user, project: project) }
|
||||
let(:snippet_changes) { "#{TestEnv::BRANCH_SHA['snippet/single-file']} #{TestEnv::BRANCH_SHA['snippet/edit-file']} refs/heads/snippet/edit-file" }
|
||||
|
||||
describe "GET /internal/check" do
|
||||
|
|
|
@ -164,6 +164,30 @@ describe API::ProjectSnippets do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with an external user' do
|
||||
let(:user) { create(:user, :external) }
|
||||
|
||||
context 'that belongs to the project' do
|
||||
before do
|
||||
project.add_developer(user)
|
||||
end
|
||||
|
||||
it 'creates a new snippet' do
|
||||
post api("/projects/#{project.id}/snippets/", user), params: params
|
||||
|
||||
expect(response).to have_gitlab_http_status(:created)
|
||||
end
|
||||
end
|
||||
|
||||
context 'that does not belong to the project' do
|
||||
it 'does not create a new snippet' do
|
||||
post api("/projects/#{project.id}/snippets/", user), params: params
|
||||
|
||||
expect(response).to have_gitlab_http_status(:forbidden)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a regular user' do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
|
|
|
@ -275,6 +275,18 @@ describe API::Repositories do
|
|||
|
||||
expect(response).to have_gitlab_http_status(:too_many_requests)
|
||||
end
|
||||
|
||||
context "when hotlinking detection is enabled" do
|
||||
before do
|
||||
Feature.enable(:repository_archive_hotlinking_interception)
|
||||
end
|
||||
|
||||
it_behaves_like "hotlink interceptor" do
|
||||
let(:http_request) do
|
||||
get api(route, current_user), headers: headers
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when unauthenticated', 'and project is public' do
|
||||
|
|
|
@ -266,6 +266,16 @@ describe API::Snippets do
|
|||
|
||||
it_behaves_like 'snippet creation'
|
||||
|
||||
context 'with an external user' do
|
||||
let(:user) { create(:user, :external) }
|
||||
|
||||
it 'does not create a new snippet' do
|
||||
post api("/snippets/", user), params: params
|
||||
|
||||
expect(response).to have_gitlab_http_status(:forbidden)
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns 400 for missing parameters' do
|
||||
params.delete(:title)
|
||||
|
||||
|
|
|
@ -238,24 +238,44 @@ describe API::Triggers do
|
|||
end
|
||||
|
||||
describe 'PUT /projects/:id/triggers/:trigger_id' do
|
||||
context 'authenticated user with valid permissions' do
|
||||
let(:new_description) { 'new description' }
|
||||
context 'user is maintainer of the project' do
|
||||
context 'the trigger belongs to user' do
|
||||
let(:new_description) { 'new description' }
|
||||
|
||||
it 'updates description' do
|
||||
put api("/projects/#{project.id}/triggers/#{trigger.id}", user),
|
||||
params: { description: new_description }
|
||||
it 'updates description' do
|
||||
put api("/projects/#{project.id}/triggers/#{trigger.id}", user),
|
||||
params: { description: new_description }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response).to include('description' => new_description)
|
||||
expect(trigger.reload.description).to eq(new_description)
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response).to include('description' => new_description)
|
||||
expect(trigger.reload.description).to eq(new_description)
|
||||
end
|
||||
end
|
||||
|
||||
context 'the trigger does not belong to user' do
|
||||
it 'does not update trigger' do
|
||||
put api("/projects/#{project.id}/triggers/#{trigger2.id}", user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:forbidden)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'authenticated user with invalid permissions' do
|
||||
it 'does not update trigger' do
|
||||
put api("/projects/#{project.id}/triggers/#{trigger.id}", user2)
|
||||
context 'user is developer of the project' do
|
||||
context 'the trigger belongs to user' do
|
||||
it 'does not update trigger' do
|
||||
put api("/projects/#{project.id}/triggers/#{trigger2.id}", user2)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:forbidden)
|
||||
expect(response).to have_gitlab_http_status(:forbidden)
|
||||
end
|
||||
end
|
||||
|
||||
context 'the trigger does not belong to user' do
|
||||
it 'does not update trigger' do
|
||||
put api("/projects/#{project.id}/triggers/#{trigger.id}", user2)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:forbidden)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -25,6 +25,17 @@ describe JwtController do
|
|||
end
|
||||
|
||||
context 'when using authenticated request' do
|
||||
shared_examples 'rejecting a blocked user' do
|
||||
context 'with blocked user' do
|
||||
let(:user) { create(:user, :blocked) }
|
||||
|
||||
it 'rejects the request as unauthorized' do
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
expect(response.body).to include('HTTP Basic: Access denied')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'using CI token' do
|
||||
let(:build) { create(:ci_build, :running) }
|
||||
let(:project) { build.project }
|
||||
|
@ -61,6 +72,8 @@ describe JwtController do
|
|||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(service_class).to have_received(:new).with(nil, user, ActionController::Parameters.new(parameters).permit!)
|
||||
end
|
||||
|
||||
it_behaves_like 'rejecting a blocked user'
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -72,6 +85,8 @@ describe JwtController do
|
|||
|
||||
it { expect(service_class).to have_received(:new).with(nil, user, ActionController::Parameters.new(parameters).permit!) }
|
||||
|
||||
it_behaves_like 'rejecting a blocked user'
|
||||
|
||||
context 'when passing a flat array of scopes' do
|
||||
# We use this trick to make rails to generate a query_string:
|
||||
# scope=scope1&scope=scope2
|
||||
|
|
|
@ -6,9 +6,9 @@ describe 'Git LFS API and storage' do
|
|||
include ProjectForksHelper
|
||||
include WorkhorseHelpers
|
||||
|
||||
set(:project) { create(:project, :repository) }
|
||||
set(:other_project) { create(:project, :repository) }
|
||||
set(:user) { create(:user) }
|
||||
let_it_be(:project, reload: true) { create(:project, :repository) }
|
||||
let_it_be(:other_project) { create(:project, :repository) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
let!(:lfs_object) { create(:lfs_object, :with_file) }
|
||||
|
||||
let(:headers) do
|
||||
|
|
|
@ -138,7 +138,7 @@ describe MergeRequestPollWidgetEntity do
|
|||
end
|
||||
|
||||
describe 'pipeline' do
|
||||
let(:pipeline) { create(:ci_empty_pipeline, project: project, ref: resource.source_branch, sha: resource.source_branch_sha, head_pipeline_of: resource) }
|
||||
let!(:pipeline) { create(:ci_empty_pipeline, project: project, ref: resource.source_branch, sha: resource.source_branch_sha, head_pipeline_of: resource) }
|
||||
|
||||
before do
|
||||
allow_any_instance_of(MergeRequestPresenter).to receive(:can?).and_call_original
|
||||
|
@ -158,6 +158,10 @@ describe MergeRequestPollWidgetEntity do
|
|||
|
||||
expect(subject[:pipeline]).to eq(pipeline_payload)
|
||||
end
|
||||
|
||||
it 'returns ci_status' do
|
||||
expect(subject[:ci_status]).to eq('pending')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when is not up to date' do
|
||||
|
@ -171,10 +175,15 @@ describe MergeRequestPollWidgetEntity do
|
|||
|
||||
context 'when user does not have access to pipelines' do
|
||||
let(:result) { false }
|
||||
let(:req) { double('request', current_user: user, project: project) }
|
||||
|
||||
it 'does not have pipeline' do
|
||||
expect(subject[:pipeline]).to eq(nil)
|
||||
end
|
||||
|
||||
it 'does not return ci_status' do
|
||||
expect(subject[:ci_status]).to eq(nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,9 +5,9 @@ require 'spec_helper'
|
|||
describe Metrics::Dashboard::UpdateDashboardService, :use_clean_rails_memory_store_caching do
|
||||
include MetricsDashboardHelpers
|
||||
|
||||
set(:user) { create(:user) }
|
||||
set(:project) { create(:project, :repository) }
|
||||
set(:environment) { create(:environment, project: project) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:project) { create(:project, :repository) }
|
||||
let_it_be(:environment) { create(:environment, project: project) }
|
||||
|
||||
describe '#execute' do
|
||||
subject(:service_call) { described_class.new(project, user, params).execute }
|
||||
|
|
|
@ -68,6 +68,21 @@ module PrometheusHelpers
|
|||
})
|
||||
end
|
||||
|
||||
def stub_prometheus_query_error(url, error_message = 'error', body: {}, headers: {})
|
||||
response = {
|
||||
status: 'error',
|
||||
errorType: 'bad_data',
|
||||
error: error_message
|
||||
}.merge(body)
|
||||
|
||||
WebMock.stub_request(:get, url)
|
||||
.to_return({
|
||||
status: 400,
|
||||
headers: { 'Content-Type' => 'application/json' }.merge(headers),
|
||||
body: response.to_json
|
||||
})
|
||||
end
|
||||
|
||||
def stub_prometheus_request_with_exception(url, exception_type)
|
||||
WebMock.stub_request(:get, url).to_raise(exception_type)
|
||||
end
|
||||
|
|
|
@ -76,7 +76,7 @@ module WorkhorseHelpers
|
|||
"#{key}.size" => file.size
|
||||
}.tap do |params|
|
||||
params["#{key}.path"] = file.path if file.path
|
||||
params["#{key}.remote_id"] = file.remote_id if file.respond_to?(:remote_id) && file.remote_id
|
||||
params["#{key}.remote_id"] = file.remote_id if file.respond_to?(:remote_id) && file.remote_id.present?
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_context 'IssuesFinder context' do
|
||||
set(:user) { create(:user) }
|
||||
set(:user2) { create(:user) }
|
||||
set(:group) { create(:group) }
|
||||
set(:subgroup) { create(:group, parent: group) }
|
||||
set(:project1) { create(:project, group: group) }
|
||||
set(:project2) { create(:project) }
|
||||
set(:project3) { create(:project, group: subgroup) }
|
||||
set(:milestone) { create(:milestone, project: project1) }
|
||||
set(:label) { create(:label, project: project2) }
|
||||
set(:label2) { create(:label, project: project2) }
|
||||
set(:issue1) { create(:issue, author: user, assignees: [user], project: project1, milestone: milestone, title: 'gitlab', created_at: 1.week.ago, updated_at: 1.week.ago) }
|
||||
set(:issue2) { create(:issue, author: user, assignees: [user], project: project2, description: 'gitlab', created_at: 1.week.from_now, updated_at: 1.week.from_now) }
|
||||
set(:issue3) { create(:issue, author: user2, assignees: [user2], project: project2, title: 'tanuki', description: 'tanuki', created_at: 2.weeks.from_now, updated_at: 2.weeks.from_now) }
|
||||
set(:issue4) { create(:issue, project: project3) }
|
||||
set(:award_emoji1) { create(:award_emoji, name: 'thumbsup', user: user, awardable: issue1) }
|
||||
set(:award_emoji2) { create(:award_emoji, name: 'thumbsup', user: user2, awardable: issue2) }
|
||||
set(:award_emoji3) { create(:award_emoji, name: 'thumbsdown', user: user, awardable: issue3) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:user2) { create(:user) }
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:subgroup) { create(:group, parent: group) }
|
||||
let_it_be(:project1, reload: true) { create(:project, group: group) }
|
||||
let_it_be(:project2, reload: true) { create(:project) }
|
||||
let_it_be(:project3, reload: true) { create(:project, group: subgroup) }
|
||||
let_it_be(:milestone) { create(:milestone, project: project1) }
|
||||
let_it_be(:label) { create(:label, project: project2) }
|
||||
let_it_be(:label2) { create(:label, project: project2) }
|
||||
let_it_be(:issue1, reload: true) { create(:issue, author: user, assignees: [user], project: project1, milestone: milestone, title: 'gitlab', created_at: 1.week.ago, updated_at: 1.week.ago) }
|
||||
let_it_be(:issue2, reload: true) { create(:issue, author: user, assignees: [user], project: project2, description: 'gitlab', created_at: 1.week.from_now, updated_at: 1.week.from_now) }
|
||||
let_it_be(:issue3, reload: true) { create(:issue, author: user2, assignees: [user2], project: project2, title: 'tanuki', description: 'tanuki', created_at: 2.weeks.from_now, updated_at: 2.weeks.from_now) }
|
||||
let_it_be(:issue4, reload: true) { create(:issue, project: project3) }
|
||||
let_it_be(:award_emoji1) { create(:award_emoji, name: 'thumbsup', user: user, awardable: issue1) }
|
||||
let_it_be(:award_emoji2) { create(:award_emoji, name: 'thumbsup', user: user2, awardable: issue2) }
|
||||
let_it_be(:award_emoji3) { create(:award_emoji, name: 'thumbsdown', user: user, awardable: issue3) }
|
||||
end
|
||||
|
||||
RSpec.shared_context 'IssuesFinder#execute context' do
|
||||
|
|
|
@ -13,15 +13,14 @@ RSpec.shared_context 'MergeRequestsFinder multiple projects with merge requests
|
|||
end
|
||||
end
|
||||
|
||||
set(:user) { create(:user) }
|
||||
set(:user2) { create(:user) }
|
||||
|
||||
set(:group) { create(:group) }
|
||||
set(:subgroup) { create(:group, parent: group) }
|
||||
set(:project1) do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:user2) { create(:user) }
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:subgroup) { create(:group, parent: group) }
|
||||
let_it_be(:project1, reload: true) do
|
||||
allow_gitaly_n_plus_1 { create(:project, :public, group: group) }
|
||||
end
|
||||
# We cannot use `set` here otherwise we get:
|
||||
# We cannot use `let_it_be` here otherwise we get:
|
||||
# Failure/Error: allow(RepositoryForkWorker).to receive(:perform_async).and_return(true)
|
||||
# The use of doubles or partial doubles from rspec-mocks outside of the per-test lifecycle is not supported.
|
||||
let(:project2) do
|
||||
|
@ -36,13 +35,13 @@ RSpec.shared_context 'MergeRequestsFinder multiple projects with merge requests
|
|||
end
|
||||
end
|
||||
end
|
||||
set(:project4) do
|
||||
let_it_be(:project4, reload: true) do
|
||||
allow_gitaly_n_plus_1 { create(:project, :repository, group: subgroup) }
|
||||
end
|
||||
set(:project5) do
|
||||
let_it_be(:project5, reload: true) do
|
||||
allow_gitaly_n_plus_1 { create(:project, group: subgroup) }
|
||||
end
|
||||
set(:project6) do
|
||||
let_it_be(:project6, reload: true) do
|
||||
allow_gitaly_n_plus_1 { create(:project, group: subgroup) }
|
||||
end
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_context 'UsersFinder#execute filter by project context' do
|
||||
set(:normal_user) { create(:user, username: 'johndoe') }
|
||||
set(:blocked_user) { create(:user, :blocked, username: 'notsorandom') }
|
||||
set(:external_user) { create(:user, :external) }
|
||||
set(:omniauth_user) { create(:omniauth_user, provider: 'twitter', extern_uid: '123456') }
|
||||
let_it_be(:normal_user) { create(:user, username: 'johndoe') }
|
||||
let_it_be(:blocked_user) { create(:user, :blocked, username: 'notsorandom') }
|
||||
let_it_be(:external_user) { create(:user, :external) }
|
||||
let_it_be(:omniauth_user) { create(:omniauth_user, provider: 'twitter', extern_uid: '123456') }
|
||||
end
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_context 'gitlab email notification' do
|
||||
set(:group) { create(:group) }
|
||||
set(:subgroup) { create(:group, parent: group) }
|
||||
set(:project) { create(:project, :repository, name: 'a-known-name', group: group) }
|
||||
set(:recipient) { create(:user, email: 'recipient@example.com') }
|
||||
|
||||
let_it_be(:group, reload: true) { create(:group) }
|
||||
let_it_be(:subgroup) { create(:group, parent: group) }
|
||||
let_it_be(:project, reload: true) { create(:project, :repository, name: 'a-known-name', group: group) }
|
||||
let_it_be(:recipient, reload: true) { create(:user, email: 'recipient@example.com') }
|
||||
let(:gitlab_sender_display_name) { Gitlab.config.gitlab.email_display_name }
|
||||
let(:gitlab_sender) { Gitlab.config.gitlab.email_from }
|
||||
let(:gitlab_sender_reply_to) { Gitlab.config.gitlab.email_reply_to }
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_context 'ProjectPolicy context' do
|
||||
set(:guest) { create(:user) }
|
||||
set(:reporter) { create(:user) }
|
||||
set(:developer) { create(:user) }
|
||||
set(:maintainer) { create(:user) }
|
||||
set(:owner) { create(:user) }
|
||||
set(:admin) { create(:admin) }
|
||||
let_it_be(:guest) { create(:user) }
|
||||
let_it_be(:reporter) { create(:user) }
|
||||
let_it_be(:developer) { create(:user) }
|
||||
let_it_be(:maintainer) { create(:user) }
|
||||
let_it_be(:owner) { create(:user) }
|
||||
let_it_be(:admin) { create(:admin) }
|
||||
let(:project) { create(:project, :public, namespace: owner.namespace) }
|
||||
|
||||
let(:base_guest_permissions) do
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples "hotlink interceptor" do
|
||||
let(:http_request) { nil }
|
||||
let(:headers) { nil }
|
||||
|
||||
describe "DDOS prevention" do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
context "hotlinked as media" do
|
||||
where(:response_status, :accept_header) do
|
||||
# These are default formats in modern browsers, and IE
|
||||
:ok | "*/*"
|
||||
:ok | "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
|
||||
:ok | "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"
|
||||
:ok | "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
|
||||
:ok | "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8"
|
||||
:ok | "image/jpeg, application/x-ms-application, image/gif, application/xaml+xml, image/pjpeg, application/x-ms-xbap, application/x-shockwave-flash, application/msword, */*"
|
||||
:ok | "text/html, application/xhtml+xml, image/jxr, */*"
|
||||
:ok | "text/html, application/xml;q=0.9, application/xhtml+xml, image/png, image/webp, image/jpeg, image/gif, image/x-xbitmap, */*;q=0.1"
|
||||
|
||||
# These are image request formats
|
||||
:not_acceptable | "image/webp,*/*"
|
||||
:not_acceptable | "image/png,image/*;q=0.8,*/*;q=0.5"
|
||||
:not_acceptable | "image/webp,image/apng,image/*,*/*;q=0.8"
|
||||
:not_acceptable | "image/png,image/svg+xml,image/*;q=0.8, */*;q=0.5"
|
||||
|
||||
# Video request formats
|
||||
:not_acceptable | "video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5"
|
||||
|
||||
# Audio request formats
|
||||
:not_acceptable | "audio/webm,audio/ogg,audio/wav,audio/*;q=0.9,application/ogg;q=0.7,video/*;q=0.6,*/*;q=0.5"
|
||||
|
||||
# CSS request formats
|
||||
:not_acceptable | "text/css,*/*;q=0.1"
|
||||
:not_acceptable | "text/css"
|
||||
:not_acceptable | "text/css,*/*;q=0.1"
|
||||
end
|
||||
|
||||
with_them do
|
||||
let(:headers) do
|
||||
{ "Accept" => accept_header }
|
||||
end
|
||||
|
||||
before do
|
||||
request.headers.merge!(headers) if request.present?
|
||||
end
|
||||
|
||||
it "renders the response" do
|
||||
http_request
|
||||
|
||||
expect(response).to have_gitlab_http_status(response_status)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "hotlinked as a script" do
|
||||
where(:response_status, :fetch_mode) do
|
||||
# Standard navigation fetch modes
|
||||
:ok | "navigate"
|
||||
:ok | "nested-navigate"
|
||||
:ok | "same-origin"
|
||||
|
||||
# Fetch modes when linking as JS
|
||||
:not_acceptable | "cors"
|
||||
:not_acceptable | "no-cors"
|
||||
:not_acceptable | "websocket"
|
||||
end
|
||||
|
||||
with_them do
|
||||
let(:headers) do
|
||||
{ "Sec-Fetch-Mode" => fetch_mode }
|
||||
end
|
||||
|
||||
before do
|
||||
request.headers.merge!(headers) if request.present?
|
||||
end
|
||||
|
||||
it "renders the response" do
|
||||
http_request
|
||||
|
||||
expect(response).to have_gitlab_http_status(response_status)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -8,12 +8,12 @@ RSpec.shared_examples 'snippet visibility' do
|
|||
DatabaseCleaner.clean_with(:truncation)
|
||||
end
|
||||
|
||||
set(:author) { create(:user) }
|
||||
set(:member) { create(:user) }
|
||||
set(:external) { create(:user, :external) }
|
||||
set(:non_member) { create(:user) }
|
||||
let_it_be(:author) { create(:user) }
|
||||
let_it_be(:member) { create(:user) }
|
||||
let_it_be(:external) { create(:user, :external) }
|
||||
let_it_be(:non_member) { create(:user) }
|
||||
|
||||
set(:project) do
|
||||
let_it_be(:project, reload: true) do
|
||||
create(:project).tap do |project|
|
||||
project.add_developer(author)
|
||||
project.add_developer(member)
|
||||
|
|
|
@ -224,7 +224,7 @@ RSpec.shared_examples 'issuable quick actions' do
|
|||
end
|
||||
|
||||
context 'when user can update issuable' do
|
||||
set(:developer) { create(:user) }
|
||||
let_it_be(:developer) { create(:user) }
|
||||
let(:note_author) { developer }
|
||||
|
||||
before do
|
||||
|
@ -251,7 +251,7 @@ RSpec.shared_examples 'issuable quick actions' do
|
|||
end
|
||||
|
||||
context 'when user cannot update issuable' do
|
||||
set(:non_member) { create(:user) }
|
||||
let_it_be(:non_member) { create(:user) }
|
||||
let(:note_author) { non_member }
|
||||
|
||||
it 'applies commands that user can execute' do
|
||||
|
|
|
@ -165,7 +165,7 @@ RSpec.shared_examples 'group and project boards' do |route_definition, ee = fals
|
|||
end
|
||||
|
||||
context "when the user is parent owner" do
|
||||
set(:owner) { create(:user) }
|
||||
let_it_be(:owner, reload: true) { create(:user) }
|
||||
|
||||
before do
|
||||
if board_parent.try(:namespace)
|
||||
|
|
|
@ -714,6 +714,19 @@ describe ObjectStorage do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when empty remote_id is specified' do
|
||||
let(:uploaded_file) do
|
||||
UploadedFile.new(temp_file.path, remote_id: '')
|
||||
end
|
||||
|
||||
it 'uses local storage' do
|
||||
subject
|
||||
|
||||
expect(uploader).to be_file_storage
|
||||
expect(uploader.object_store).to eq(described_class::Store::LOCAL)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when valid file is specified' do
|
||||
let(:uploaded_file) do
|
||||
UploadedFile.new(temp_file.path, filename: "my_file.txt", remote_id: "test/123123")
|
||||
|
|
Loading…
Reference in New Issue