Commit db0d1c9b authored by Mayra Cabrera's avatar Mayra Cabrera

Merge remote-tracking branch 'dev/master'

parents 54b12e93 9458f7c5
Please view this file on the master branch, on stable branches it's out of date.
## 12.8.2
### Security (5 changes)
- Don't show Contribution Analytics to users who are not group members.
- Update epic tree when group is transfered.
- Fix Service Side Request Forgery in JenkinsDeprecatedService.
- Enforce vulnerability feedback pipeline is in the same project.
- Enforce existing vulnerability feedback pipeline is in the same project.
## 12.8.1
### Performance (1 change)
......
......@@ -2,6 +2,33 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 12.8.2
### Security (17 changes)
- Update container registry authentication to account for login request when checking permissions.
- Update ProjectAuthorization when deleting or updating GroupGroupLink.
- Prevent an endless checking loop for two merge requests targeting each other.
- Update user 2fa when accepting a group invite.
- Fix for XSS in branch names.
- Prevent directory traversal through FileUploader.
- Run project badge images through the asset proxy.
- Check merge requests read permissions before showing them in the pipeline widget.
- Respect member access level for group shares.
- Remove OID filtering during LFS imports.
- Protect against denial of service using pipeline webhook recursion.
- Expire account confirmation token.
- Prevent XSS in admin grafana URL setting.
- Don't require base_sha in DiffRefsType.
- Sanitize output by dependency linkers.
- Recalculate ProjectAuthorizations for all users.
- Escape special chars in Sentry error header.
### Other (1 change, 1 of them is from the community)
- Fix fixtures for Error Tracking Web UI. !26233 (Takuya Noguchi)
## 12.8.1
### Fixed (5 changes)
......
......@@ -108,16 +108,6 @@ export default {
'errorStatus',
]),
...mapGetters('details', ['stacktrace']),
reported() {
return sprintf(
__('Reported %{timeAgo} by %{reportedBy}'),
{
reportedBy: `<strong class="error-details-meta-culprit">${this.error.culprit}</strong>`,
timeAgo: this.timeFormatted(this.stacktraceData.date_received),
},
false,
);
},
firstReleaseLink() {
return `${this.error.externalBaseUrl}/releases/${this.error.firstReleaseShortVersion}`;
},
......@@ -227,8 +217,19 @@ export default {
</gl-alert>
<div class="error-details-header d-flex py-2 justify-content-between">
<div class="error-details-meta my-auto">
<span v-if="!loadingStacktrace && stacktrace" v-html="reported"></span>
<div
v-if="!loadingStacktrace && stacktrace"
class="error-details-meta my-auto"
data-qa-selector="reported_text"
>
<gl-sprintf :message="__('Reported %{timeAgo} by %{reportedBy}')">
<template #reportedBy>
<strong class="error-details-meta-culprit">{{ error.culprit }}</strong>
</template>
<template #timeAgo>
{{ timeFormatted(stacktraceData.date_received) }}
</template>
</gl-sprintf>
</div>
<div class="error-details-actions">
<div class="d-inline-flex bv-d-sm-down-none">
......
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import { escape } from 'lodash';
import simplePoll from '../../../lib/utils/simple_poll';
import eventHub from '../../event_hub';
import statusIcon from '../mr_widget_status_icon.vue';
......@@ -44,11 +45,10 @@ export default {
fastForwardMergeText() {
return sprintf(
__(
`Fast-forward merge is not possible. Rebase the source branch onto %{startTag}${this.mr.targetBranch}%{endTag} to allow this merge request to be merged.`,
'Fast-forward merge is not possible. Rebase the source branch onto %{targetBranch} to allow this merge request to be merged.',
),
{
startTag: '<span class="label-branch">',
endTag: '</span>',
targetBranch: `<span class="label-branch">${escape(this.mr.targetBranch)}</span>`,
},
false,
);
......
......@@ -9,6 +9,7 @@ module UploadsActions
included do
prepend_before_action :set_request_format_from_path_extension
rescue_from FileUploader::InvalidSecret, with: :render_404
end
def create
......
......@@ -24,7 +24,7 @@ class Groups::GroupLinksController < Groups::ApplicationController
end
def update
@group_link.update(group_link_params)
Groups::GroupLinks::UpdateService.new(@group_link).execute(group_link_params)
end
def destroy
......
......@@ -8,7 +8,7 @@ module Types
field :head_sha, GraphQL::STRING_TYPE, null: false,
description: 'SHA of the HEAD at the time the comment was made'
field :base_sha, GraphQL::STRING_TYPE, null: false,
field :base_sha, GraphQL::STRING_TYPE, null: true,
description: 'Merge base of the branch the comment was made on'
field :start_sha, GraphQL::STRING_TYPE, null: false,
description: 'SHA of the branch being compared against'
......
......@@ -6,6 +6,9 @@ class ApplicationSetting < ApplicationRecord
include TokenAuthenticatable
include ChronicDurationAttribute
GRAFANA_URL_ERROR_MESSAGE = 'Please check your Grafana URL setting in ' \
'Admin Area > Settings > Metrics and profiling > Metrics - Grafana'
add_authentication_token_field :runners_registration_token, encrypted: -> { Feature.enabled?(:application_settings_tokens_optional_encryption, default_enabled: true) ? :optional : :required }
add_authentication_token_field :health_check_access_token
add_authentication_token_field :static_objects_external_storage_auth_token
......@@ -38,6 +41,14 @@ class ApplicationSetting < ApplicationRecord
chronic_duration_attr_writer :archive_builds_in_human_readable, :archive_builds_in_seconds
validates :grafana_url,
system_hook_url: {
blocked_message: "is blocked: %{exception_message}. " + GRAFANA_URL_ERROR_MESSAGE
},
if: :grafana_url_absolute?
validate :validate_grafana_url
validates :uuid, presence: true
validates :outbound_local_requests_whitelist,
......@@ -362,6 +373,19 @@ class ApplicationSetting < ApplicationRecord
end
after_commit :expire_performance_bar_allowed_user_ids_cache, if: -> { previous_changes.key?('performance_bar_allowed_group_id') }
def validate_grafana_url
unless parsed_grafana_url
self.errors.add(
:grafana_url,
"must be a valid relative or absolute URL. #{GRAFANA_URL_ERROR_MESSAGE}"
)
end
end
def grafana_url_absolute?
parsed_grafana_url&.absolute?
end
def sourcegraph_url_is_com?
!!(sourcegraph_url =~ /\Ahttps:\/\/(www\.)?sourcegraph\.com/)
end
......@@ -394,6 +418,12 @@ class ApplicationSetting < ApplicationRecord
rescue RegexpError
errors.add(:email_restrictions, _('is not a valid regular expression'))
end
private
def parsed_grafana_url
@parsed_grafana_url ||= Gitlab::Utils.parse_url(grafana_url)
end
end
ApplicationSetting.prepend_if_ee('EE::ApplicationSetting')
......@@ -32,7 +32,9 @@ class Badge < ApplicationRecord
end
def rendered_image_url(project = nil)
build_rendered_url(image_url, project)
Gitlab::AssetProxy.proxy_url(
build_rendered_url(image_url, project)
)
end
private
......
......@@ -516,18 +516,29 @@ class Group < Namespace
group_group_links_query = GroupGroupLink.where(shared_group_id: self_and_ancestors_ids)
cte = Gitlab::SQL::CTE.new(:group_group_links_cte, group_group_links_query)
cte_alias = cte.table.alias(GroupGroupLink.table_name)
link = GroupGroupLink
.with(cte.to_arel)
.select(smallest_value_arel([cte_alias[:group_access], group_member_table[:access_level]],
'group_access'))
.from([group_member_table, cte.alias_to(group_group_link_table)])
.where(group_member_table[:user_id].eq(user.id))
.where(group_member_table[:requested_at].eq(nil))
.where(group_member_table[:source_id].eq(group_group_link_table[:shared_with_group_id]))
.where(group_member_table[:source_type].eq('Namespace'))
.reorder(Arel::Nodes::Descending.new(group_group_link_table[:group_access]))
.first
link&.group_access
end
def smallest_value_arel(args, column_alias)
Arel::Nodes::As.new(
Arel::Nodes::NamedFunction.new('LEAST', args),
Arel::Nodes::SqlLiteral.new(column_alias))
end
def self.groups_including_descendants_by(group_ids)
Gitlab::ObjectHierarchy
.new(Group.where(id: group_ids))
......
......@@ -66,6 +66,7 @@ class GroupMember < Member
def after_accept_invite
notification_service.accept_group_invite(self)
update_two_factor_requirement
super
end
......
......@@ -134,7 +134,11 @@ module Ci
def all_related_merge_requests
strong_memoize(:all_related_merge_requests) do
pipeline.ref ? pipeline.all_merge_requests_by_recency.to_a : []
if pipeline.ref && can?(current_user, :read_merge_request, pipeline.project)
pipeline.all_merge_requests_by_recency.to_a
else
[]
end
end
end
end
......
......@@ -3,12 +3,24 @@
module Auth
class ContainerRegistryAuthenticationService < BaseService
AUDIENCE = 'container_registry'
REGISTRY_LOGIN_ABILITIES = [
:read_container_image,
:create_container_image,
:destroy_container_image,
:update_container_image,
:admin_container_image,
:build_read_container_image,
:build_create_container_image,
:build_destroy_container_image
].freeze
def execute(authentication_abilities:)
@authentication_abilities = authentication_abilities
return error('UNAVAILABLE', status: 404, message: 'registry not enabled') unless registry.enabled
return error('DENIED', status: 403, message: 'access forbidden') unless has_registry_ability?
unless scopes.any? || current_user || project
return error('DENIED', status: 403, message: 'access forbidden')
end
......@@ -197,5 +209,11 @@ module Auth
def has_authentication_ability?(capability)
@authentication_abilities.to_a.include?(capability)
end
def has_registry_ability?
@authentication_abilities.any? do |ability|
REGISTRY_LOGIN_ABILITIES.include?(ability)
end
end
end
end
......@@ -6,19 +6,17 @@ module Groups
def execute(one_or_more_links)
links = Array(one_or_more_links)
GroupGroupLink.transaction do
GroupGroupLink.delete(links)
if GroupGroupLink.delete(links)
Gitlab::AppLogger.info(
"GroupGroupLinks with ids: #{links.map(&:id)} have been deleted.")
groups_to_refresh = links.map(&:shared_with_group)
groups_to_refresh.uniq.each do |group|
group.refresh_members_authorized_projects
end
Gitlab::AppLogger.info("GroupGroupLinks with ids: #{links.map(&:id)} have been deleted.")
rescue => ex
Gitlab::AppLogger.error(ex)
raise
else
Gitlab::AppLogger.info(
"Failed to delete GroupGroupLinks with ids: #{links.map(&:id)}.")
end
end
end
......
# frozen_string_literal: true
module Groups
module GroupLinks
class UpdateService < BaseService
def initialize(group_link, user = nil)
super(group_link.shared_group, user)
@group_link = group_link
end
def execute(group_link_params)
group_link.update!(group_link_params)
if requires_authorization_refresh?(group_link_params)
group_link.shared_with_group.refresh_members_authorized_projects
end
end
private
attr_accessor :group_link
def requires_authorization_refresh?(params)
params.include?(:group_access)
end
end
end
end
......@@ -16,17 +16,14 @@ module Projects
@lfs_download_object = lfs_download_object
end
# rubocop: disable CodeReuse/ActiveRecord
def execute
return unless project&.lfs_enabled? && lfs_download_object
return error("LFS file with oid #{lfs_oid} has invalid attributes") unless lfs_download_object.valid?
return if LfsObject.exists?(oid: lfs_oid)
wrap_download_errors do
download_lfs_file!
end
end
# rubocop: enable CodeReuse/ActiveRecord
private
......@@ -39,14 +36,24 @@ module Projects
def download_lfs_file!
with_tmp_file do |tmp_file|
download_and_save_file!(tmp_file)
project.lfs_objects << LfsObject.new(oid: lfs_oid,
size: lfs_size,
file: tmp_file)
project.lfs_objects << find_or_create_lfs_object(tmp_file)
success
end
end
def find_or_create_lfs_object(tmp_file)
lfs_obj = LfsObject.safe_find_or_create_by!(
oid: lfs_oid,
size: lfs_size
)
lfs_obj.update!(file: tmp_file) unless lfs_obj.file.file
lfs_obj
end
def download_and_save_file!(file)
digester = Digest::SHA256.new
response = Gitlab::HTTP.get(lfs_sanitized_url, download_headers) do |fragment|
......
......@@ -26,12 +26,12 @@ module Projects
return []
end
# Getting all Lfs pointers already in the database and linking them to the project
linked_oids = LfsLinkService.new(project).execute(lfs_pointers_in_repository.keys)
# Retrieving those oids not present in the database which we need to download
missing_oids = lfs_pointers_in_repository.except(*linked_oids)
# Downloading the required information and gathering it inside a LfsDownloadObject for each oid
LfsDownloadLinkListService.new(project, remote_uri: current_endpoint_uri).execute(missing_oids)
# Downloading the required information and gathering it inside an
# LfsDownloadObject for each oid
#
LfsDownloadLinkListService
.new(project, remote_uri: current_endpoint_uri)
.execute(lfs_pointers_in_repository)
rescue LfsDownloadLinkListService::DownloadLinksError => e
raise LfsObjectDownloadListError, "The LFS objects download list couldn't be imported. Error: #{e.message}"
end
......
......@@ -13,8 +13,14 @@ class WebHookService
end
end
GITLAB_EVENT_HEADER = 'X-Gitlab-Event'
attr_accessor :hook, :data, :hook_name, :request_options
def self.hook_to_event(hook_name)
hook_name.to_s.singularize.titleize
end
def initialize(hook, data, hook_name)
@hook = hook
@data = data
......@@ -112,7 +118,7 @@ class WebHookService
@headers ||= begin
{
'Content-Type' => 'application/json',
'X-Gitlab-Event' => hook_name.singularize.titleize
GITLAB_EVENT_HEADER => self.class.hook_to_event(hook_name)
}.tap do |hash|
hash['X-Gitlab-Token'] = Gitlab::Utils.remove_line_breaks(hook.token) if hook.token.present?
end
......
......@@ -16,6 +16,9 @@ class FileUploader < GitlabUploader
MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)}.freeze
DYNAMIC_PATH_PATTERN = %r{.*(?<secret>\h{32})/(?<identifier>.*)}.freeze
VALID_SECRET_PATTERN = %r{\A\h{10,32}\z}.freeze
InvalidSecret = Class.new(StandardError)
after :remove, :prune_store_dir
......@@ -153,6 +156,10 @@ class FileUploader < GitlabUploader
def secret
@secret ||= self.class.generate_secret
raise InvalidSecret unless @secret =~ VALID_SECRET_PATTERN
@secret
end
# return a new uploader with a file copy on another project
......
......@@ -23,7 +23,8 @@
# protect against Server-side Request Forgery (SSRF), or check for the right port.
#
# Configuration options:
# * <tt>message</tt> - A custom error message (default is: "must be a valid URL").
# * <tt>message</tt> - A custom error message, used when the URL is blank. (default is: "must be a valid URL").
# * <tt>blocked_message</tt> - A custom error message, used when the URL is blocked. Default: +'is blocked: %{exception_message}'+.
# * <tt>schemes</tt> - Array of URI schemes. Default: +['http', 'https']+
# * <tt>allow_localhost</tt> - Allow urls pointing to +localhost+. Default: +true+
# * <tt>allow_local_network</tt> - Allow urls pointing to private network addresses. Default: +true+
......@@ -59,7 +60,8 @@ class AddressableUrlValidator < ActiveModel::EachValidator
}.freeze
DEFAULT_OPTIONS = BLOCKER_VALIDATE_OPTIONS.merge({
message: 'must be a valid URL'
message: 'must be a valid URL',
blocked_message: 'is blocked: %{exception_message}'
}).freeze
def initialize(options)
......@@ -80,7 +82,7 @@ class AddressableUrlValidator < ActiveModel::EachValidator
Gitlab::UrlBlocker.validate!(value, blocker_args)
rescue Gitlab::UrlBlocker::BlockedUrlError => e
record.errors.add(attribute, "is blocked: #{e.message}")
record.errors.add(attribute, options.fetch(:blocked_message) % { exception_message: e.message })
end
private
......
= form_for @application_setting, url: admin_application_settings_path(anchor: 'js-grafana-settings'), html: { class: 'fieldset-form' } do |f|
= form_for @application_setting, url: metrics_and_profiling_admin_application_settings_path(anchor: 'js-grafana-settings'), html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
......
......@@ -83,7 +83,7 @@
= _('Requests Profiles')
- if Gitlab::CurrentSettings.current_application_settings.grafana_enabled?
= nav_link do
= link_to Gitlab::CurrentSettings.current_application_settings.grafana_url, target: '_blank', title: _('Metrics Dashboard') do
= link_to Gitlab::CurrentSettings.current_application_settings.grafana_url, target: '_blank', title: _('Metrics Dashboard'), rel: 'noopener noreferrer' do
%span
= _('Metrics Dashboard')
= render_if_exists 'layouts/nav/ee/admin/new_monitoring_sidebar'
......
......@@ -8,7 +8,9 @@
.form-group.row.d-flex.gl-pl-3.gl-pr-3.branch-selector
.align-self-center
%span= s_('From %{source_title} into').html_safe % { source_title: "<code>#{source_title}</code>".html_safe }
%span
= _('From <code>%{source_title}</code> into').html_safe % { source_title: source_title }
- if issuable.new_record?
%code= target_title
&nbsp;
......
......@@ -80,8 +80,16 @@ Devise.setup do |config|
# When allow_unconfirmed_access_for is zero, the user won't be able to sign in without confirming.
# You can use this to let your user access some features of your application
# without confirming the account, but blocking it after a certain period
# (ie 2 days).
config.allow_unconfirmed_access_for = 30.days
# (e.g. 3 days).
config.allow_unconfirmed_access_for = 3.days
# A period that the user is allowed to confirm their account before their
# token becomes invalid. For example, if set to 1.day, the user can confirm
# their account within 1 days after the mail was sent, but on the second day
# their account can't be confirmed with the token any more.
# Default is nil, meaning there is no restriction on how long a user can take
# before confirming their account.
config.confirm_within = 1.day
# Defines which key will be used when confirming an account
# config.confirmation_keys = [ :email ]
......
# frozen_string_literal: true
class CleanGrafanaUrl < ActiveRecord::Migration[6.0]
DOWNTIME = false
def up
execute(
<<-SQL
UPDATE
application_settings
SET
grafana_url = default
WHERE
position('javascript:' IN btrim(application_settings.grafana_url)) = 1
SQL
)
end
def down
# no-op
end
end
# frozen_string_literal: true
class ScheduleRecalculateProjectAuthorizationsSecondRun < ActiveRecord::Migration[5.1]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
MIGRATION = 'RecalculateProjectAuthorizationsWithMinMaxUserId'
BATCH_SIZE = 2_500
DELAY_INTERVAL = 2.minutes.to_i
disable_ddl_transaction!
class User < ActiveRecord::Base
include ::EachBatch
self.table_name = 'users'
end
def up
say "Scheduling #{MIGRATION} jobs"
User.each_batch(of: BATCH_SIZE) do |batch, index|
delay = index * DELAY_INTERVAL
range = batch.pluck('MIN(id)', 'MAX(id)').first
BackgroundMigrationWorker.perform_in(delay, MIGRATION, range)
end
end
def down
end
end
......@@ -117,8 +117,9 @@ If you have set up Grafana, you can enable a link to access it easily from the s
1. Expand **Metrics - Grafana**.
1. Check the "Enable access to Grafana" checkbox.
1. If Grafana is enabled through Omnibus GitLab and on the same server,
leave "Grafana URL" unchanged. In any other case, enter the full URL
path of the Grafana instance.
leave **Grafana URL** unchanged. It should be `/-/grafana`.
In any other case, enter the full URL of the Grafana instance.
1. Click **Save changes**.
1. The new link will be available in the **Admin Area > Monitoring > Metrics Dashboard**.
......
......@@ -1665,7 +1665,7 @@ type DiffRefs {
"""
Merge base of the branch the comment was made on
"""
baseSha: String!
baseSha: String
"""
SHA of the HEAD at the time the comment was made
......
......@@ -8546,13 +8546,9 @@
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
......
......@@ -286,7 +286,7 @@ Autogenerated return type of DestroySnippet
| Name | Type | Description |
| --- | ---- | ---------- |
| `baseSha` | String! | Merge base of the branch the comment was made on |
| `baseSha` | String | Merge base of the branch the comment was made on |
| `headSha` | String! | SHA of the HEAD at the time the comment was made |
| `startSha` | String! | SHA of the branch being compared against |
......
......@@ -3,6 +3,7 @@
class Groups::ContributionAnalyticsController < Groups::ApplicationController
before_action :group
before_action :check_contribution_analytics_available!
before_action :authorize_read_contribution_analytics!
layout 'group'
......@@ -27,6 +28,28 @@ class Groups::ContributionAnalyticsController < Groups::ApplicationController
end
def check_contribution_analytics_available!
render_404 unless @group.feature_available?(:contribution_analytics) || LicenseHelper.show_promotions?(current_user)
return if group_has_access_to_feature?
show_promotions? ? render_promotion : render_404
end
def authorize_read_contribution_analytics!
render_403 unless user_has_access_to_feature?
end
def render_promotion
render 'shared/promotions/_promote_contribution_analytics'
end
def show_promotions?
LicenseHelper.show_promotions?(current_user)
end
def group_has_access_to_feature?
@group.feature_available?(:contribution_analytics)
end
def user_has_access_to_feature?
can?(current_user, :read_group_contribution_analytics, @group)
end
end
......@@ -15,6 +15,9 @@ class Projects::VulnerabilityFeedbackController < Projects::ApplicationControlle
# TODO: Move to finder or list service
@vulnerability_feedback = @project.vulnerability_feedback.with_associations
# TODO remove once filtered data has been cleaned
@vulnerability_feedback = @vulnerability_feedback.only_valid_feedback
if params[:category].present?
@vulnerability_feedback = @vulnerability_feedback
.with_category(params[:category])
......
......@@ -188,6 +188,11 @@ module EE
::Group
end
def nullify_lost_group_parents(groups, lost_groups)
epics_to_update = in_selected_groups(groups).where(parent: in_selected_groups(lost_groups))
epics_to_update.update_all(parent_id: nil)
end
# Return the deepest relation level for an epic.
# Example 1:
# epic1 - parent: nil
......
......@@ -83,6 +83,8 @@ class JenkinsDeprecatedService < CiService
def calculate_reactive_cache(sha, ref)
{ commit_status: read_commit_status(sha, ref) }
rescue Gitlab::HTTP::BlockedUrlError => ex
Gitlab::ErrorTracking.track_exception(ex)
end
private
......@@ -91,14 +93,14 @@ class JenkinsDeprecatedService < CiService
parsed_url = URI.parse(build_page(sha, ref))
if parsed_url.userinfo.blank?
response = Gitlab::HTTP.get(build_page(sha, ref), verify: false, allow_local_requests: true)
response = Gitlab::HTTP.get(build_page(sha, ref), verify: false)
else
get_url = build_page(sha, ref).gsub("#{parsed_url.userinfo}@", "")
auth = {
username: CGI.unescape(parsed_url.user),
password: CGI.unescape(parsed_url.password)
}
response = Gitlab::HTTP.get(get_url, verify: false, basic_auth: auth, allow_local_requests: true)
response = Gitlab::HTTP.get(get_url, verify: false, basic_auth: auth)
end
if response.code == 200
......
......@@ -26,6 +26,7 @@ module Vulnerabilities
validates :feedback_type, presence: true
validates :category, presence: true
validates :project_fingerprint, presence: true, uniqueness: { scope: [:project_id, :category, :feedback_type] }
validates :pipeline, same_project_association: true, if: :pipeline_id?
scope :with_associations, -> { includes(:pipeline, :issue, :merge_request, :author, :comment_author) }
......@@ -33,6 +34,13 @@ module Vulnerabilities
preload(:author, :comment_author, :project, :issue, :merge_request, :pipeline)
end
# TODO remove once filtered data has been cleaned
def self.only_valid_feedback
pipeline = Ci::Pipeline.arel_table
feedback = arel_table
joins(:pipeline).where(pipeline[:project_id].eq(feedback[:project_id]))
end
def self.find_or_init_for(feedback_params)
validate_enums(feedback_params)
......
......@@ -75,7 +75,7 @@ module EE
rule { can?(:read_cluster) & cluster_deployments_available }
.enable :read_cluster_environments
rule { can?(:read_group) & contribution_analytics_available }
rule { has_access & contribution_analytics_available }
.enable :read_group_contribution_analytics
rule { reporter & cycle_analytics_available }.policy do
......
......@@ -9,6 +9,12 @@ module EE
group_contains_npm_packages: s_('TransferGroup|Group contains projects with NPM packages.')
}.freeze
def update_group_attributes
::Epic.nullify_lost_group_parents(group.self_and_descendants, lost_groups)
super
end
private
override :ensure_allowed_transfer
......@@ -36,6 +42,16 @@ module EE
def different_root_ancestor?
group.root_ancestor != new_parent_group&.root_ancestor
end
def lost_groups
ancestors = group.ancestors
if ancestors.include?(new_parent_group)
group.ancestors_upto(new_parent_group)
else
ancestors
end
end
end
end
end
......@@ -19,7 +19,7 @@ module VulnerabilityFeedback
vulnerability_feedback.save
end
if vulnerability_feedback.persisted?
if vulnerability_feedback.persisted? && vulnerability_feedback.valid?
success(vulnerability_feedback)
else
rollback_merge_request(vulnerability_feedback.merge_request) if vulnerability_feedback.merge_request
......
......@@ -38,6 +38,27 @@ describe Projects::VulnerabilityFeedbackController do
expect(json_response.length).to eq 5
end
# TODO remove once filtered data has been cleaned
context 'with invalid vulnerability_feedback' do
let!(:vuln_feedback_in_other_proj) do
feedback = build(
:vulnerability_feedback,
project: project,
author: user,
pipeline: create(:ci_pipeline)
)
feedback.save(validate: false)
end
it 'ignores feedback in other project' do
list_feedbacks
expect(response).to match_response_schema('vulnerability_feedback_list', dir: 'ee')
expect(json_response.length).to eq 5
end
end
context 'with filter params' do
it 'returns project feedbacks list filtered on category' do
list_feedbacks({ category: 'sast' })
......@@ -184,6 +205,39 @@ describe Projects::VulnerabilityFeedbackController do
end
end
context 'with pipeline in another project' do
let(:pipeline) { create(:ci_pipeline) }
it 'returns a 422 response' do
create_feedback user: user, project: project, params: create_params
expect(response).to have_gitlab_http_status(:unprocessable_entity)
expect(json_response).to eq({ "pipeline" => ["must associate the same project"] })
end
end
context 'with nonexistent pipeline_id' do
let(:pipeline) { build(:ci_pipeline, id: -10) }
it 'returns a 422 response' do
create_feedback user: user, project: project, params: create_params
expect(response).to have_gitlab_http_status(:unprocessable_entity)
expect(json_response).to eq({ "pipeline" => ["must associate the same project"] })
end
end
context 'with nil pipeline_id' do
let(:pipeline) { build(:ci_pipeline, id: nil) }
it 'returns a successful response' do
create_feedback user: user, project: project, params: create_params
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('vulnerability_feedback', dir: 'ee')
end
end
def create_feedback(user:, project:, params:)
sign_in(user)
post_params = {
......
......@@ -12,7 +12,7 @@ FactoryBot.define do
author
issue { nil }
merge_request { nil }
association :pipeline, factory: :ci_pipeline
pipeline { create(:ci_pipeline, project: project) }
feedback_type { 'dismissal' }
category { 'sast' }
project_fingerprint { generate(:project_fingerprint) }
......
......@@ -35,7 +35,7 @@
"enum": ["sast", "dependency_scanning", "container_scanning", "dast"]
},
"project_fingerprint": { "type": "string" },
"branch": { "type": "string" },
"branch": { "type": ["string", "null"] },
"destroy_vulnerability_feedback_dismissal_path": { "type": "string" }
},
"additionalProperties": false
......
......@@ -3,14 +3,15 @@
require 'spec_helper'
describe GroupsHelper do
let(:user) { create(:user, group_view: :security_dashboard) }
let(:owner) { create(:user, group_view: :security_dashboard) }
let(:current_user) { owner }
let(:group) { create(:group, :private) }
before do
allow(helper).to receive(:current_user) { user }
allow(helper).to receive(:current_user) { current_user }
helper.instance_variable_set(:@group, group)
group.add_owner(user)
group.add_owner(owner)
end
describe '#group_epics_count' do
......@@ -49,6 +50,21 @@ describe GroupsHelper do
expect(helper.group_sidebar_links).not_to include(:contribution_analytics, :epics)
end
context 'when contribution analytics is available' do
before do
stub_licensed_features(contribution_analytics: true)
end
context 'signed in user is a project member but not a member of the group' do
let(:current_user) { create(:user) }
let(:private_project) { create(:project, :private, group: group)}
it 'hides Contribution Analytics' do
expect(helper.group_sidebar_links).not_to include(:contribution_analytics)
end
end
end
end
describe '#permanent_deletion_date' do
......@@ -107,10 +123,10 @@ describe GroupsHelper do
with_them do
it 'returns the expected value' do
allow(helper).to receive(:current_user) { user? ? user : nil }
allow(helper).to receive(:current_user) { user? ? owner : nil }
allow(::Gitlab).to receive(:com?) { gitlab_com? }
allow(user).to receive(:ab_feature_enabled?) { ab_feature_enabled? }
allow(user).to receive(:created_at) { created_at }
allow(owner).to receive(:ab_feature_enabled?) { ab_feature_enabled? }
allow(owner).to receive(:created_at) { created_at }
allow(::Feature).to receive(:enabled?).with(:discover_security) { discover_security_feature_enabled? }
allow(group).to receive(:feature_available?) { security_dashboard_feature_available? }
allow(helper).to receive(:can?) { can_admin_group? }
......
......@@ -60,6 +60,35 @@ ICON_STATUS_HTML
expect(@service.calculate_reactive_cache('2ab7834c', 'master')).to eq(commit_status: :error)
end
end
describe 'respects outbound network setting' do
let(:url) { 'http://127.0.0.1:8080/job/test' }
before do
stub_application_setting(allow_local_requests_from_web_hooks_and_services: setting)
allow(@service).to receive(:project_url).and_return(url)
end
context 'when local requests are allowed' do
let(:setting) { true }
it "makes an outbound request" do
stub_request(:get, 'http://127.0.0.1:8080/job/test/scm/bySHA1/2ab7834c').to_return(status: 200, body: status_body_for_icon('blue.png'), headers: {})
expect(@service.calculate_reactive_cache('2ab7834c', 'master')).to eq(commit_status: 'success')
end
end
context 'when local requests are not allowed' do
let(:setting) { false }
it 'raises an exception' do
expect(Gitlab::ErrorTracking).to receive(:track_exception).with(an_instance_of(Gitlab::HTTP::BlockedUrlError)).and_call_original
expect { @service.calculate_reactive_cache('2ab7834c', 'master') }.not_to raise_error
end
end
end
end
describe '#commit_status' do
......
......@@ -28,6 +28,42 @@ describe Vulnerabilities::Feedback do
it { is_expected.to validate_presence_of(:category) }
it { is_expected.to validate_presence_of(:project_fingerprint) }
context 'pipeline is nil' do
let(:feedback) { build(:vulnerability_feedback, pipeline_id: nil) }
it 'is valid' do
expect(feedback).to be_valid
end
end
context 'pipeline has the same project_id' do
let(:feedback) { build(:vulnerability_feedback) }
it 'is valid' do
expect(feedback.project_id).to eq(feedback.pipeline.project_id)
expect(feedback).to be_valid
end
end
context 'pipeline_id does not exist' do
let(:feedback) { build(:vulnerability_feedback, pipeline_id: -100) }
it 'is invalid' do
expect(feedback.project_id).not_to eq(feedback.pipeline_id)
expect(feedback).not_to be_valid
end
end
context 'pipeline has a different project_id' do
let(:project) { create(:project) }
let(:pipeline) { create(:ci_pipeline, project: create(:project)) }
let(:feedback) { build(:vulnerability_feedback, project: project, pipeline: pipeline) }
it 'is invalid' do
expect(feedback.project_id).not_to eq(feedback.pipeline_id)
expect(feedback).not_to be_valid
end
end
context 'comment is set' do
let(:feedback) { build(:vulnerability_feedback, comment: 'a comment' ) }
......@@ -71,6 +107,26 @@ describe Vulnerabilities::Feedback do
end
end
# TODO remove once filtered data has been cleaned
describe '::only_valid_feedback' do
let(:project) { create(:project) }
let(:pipeline) { create(:ci_pipeline, project: project) }
let!(:feedback) { create(:vulnerability_feedback, :dismissal, :sast, project: project, pipeline: pipeline) }
let!(:invalid_feedback) do
feedback = build(:vulnerability_feedback, project: project, pipeline: create(:ci_pipeline))
feedback.save(validate: false)
end
it 'filters out invalid feedback' do
feedback_records = described_class.only_valid_feedback
expect(feedback_records.length).to eq 1
expect(feedback_records.first).to eq feedback
end
end
describe '#has_comment?' do
let(:feedback) { build(:vulnerability_feedback, comment: comment, comment_author: comment_author) }
let(:comment) { 'a comment' }
......
......@@ -48,7 +48,34 @@ describe GroupPolicy do
stub_licensed_features(contribution_analytics: true)
end
it { is_expected.to be_allowed(:read_group_contribution_analytics) }
context 'when signed in user is a member of the group' do
it { is_expected.to be_allowed(:read_group_contribution_analytics) }
end
describe 'when user is not a member of the group' do
let(:current_user) { non_group_member }
let(:private_group) { create(:group, :private) }
subject { described_class.new(non_group_member, private_group) }
context 'when user is not invited to any of the group projects' do
it do
is_expected.not_to be_allowed(:read_group_contribution_analytics)
end
end
context 'when user is invited to a group project, but not to the group' do
let(:private_project) { create(:project, :private, group: private_group) }
before do
private_project.add_guest(non_group_member)
end
it do
is_expected.not_to be_allowed(:read_group_contribution_analytics)
end
end
end
end
context 'when contribution analytics is not available' do
......
......@@ -97,4 +97,75 @@ describe Groups::TransferService, '#execute' do
expect(group.parent).to eq(new_group)
end
end
context 'with epics' do
context 'when epics feature is disabled' do
it 'transfers a group successfully' do
transfer_service.execute(new_group)
expect(group.parent).to eq(new_group)
end
end
context 'when epics feature is enabled' do
let(:root_group) { create(:group) }
let(:subgroup_group_level_1) { create(:group, parent: root_group) }
let(:subgroup_group_level_2) { create(:group, parent: subgroup_group_level_1) }
let(:subgroup_group_level_3) { create(:group, parent: subgroup_group_level_2) }
let!(:root_epic) { create(:epic, group: root_group) }
let!(:level_1_epic_1) { create(:epic, group: subgroup_group_level_1, parent: root_epic) }
let!(:level_1_epic_2) { create(:epic, group: subgroup_group_level_1, parent: level_1_epic_1) }
let!(:level_2_epic_1) { create(:epic, group: subgroup_group_level_2, parent: root_epic) }
let!(:level_2_epic_2) { create(:epic, group: subgroup_group_level_2, parent: level_1_epic_1) }
let!(:level_2_subepic) { create(:epic, group: subgroup_group_level_2, parent: level_2_epic_2) }
let!(:level_3_epic) { create(:epic, group: subgroup_group_level_3, parent: level_2_epic_2) }
before do
root_group.add_owner(user)
stub_licensed_features(epics: true)
end
context 'when group is moved completely out of the main group' do
let(:group) { subgroup_group_level_1 }
before do
transfer_service.execute(new_group)
end
it 'keeps relations between epics in the group structure' do
expect(level_1_epic_2.reload.parent).to eq(level_1_epic_1)
expect(level_2_epic_2.reload.parent).to eq(level_1_epic_1)
expect(level_2_subepic.reload.parent).to eq(level_2_epic_2)
expect(level_3_epic.reload.parent).to eq(level_2_epic_2)
end
it 'removes relations to epics of the old parent group' do
expect(level_1_epic_1.reload.parent).to be_nil
expect(level_2_epic_1.reload.parent).to be_nil
end
end
context 'when group is moved some levels up' do
let(:group) { subgroup_group_level_2 }
before do
transfer_service.execute(root_group)
end
it 'keeps relations between epics in the group structure' do
expect(level_1_epic_1.reload.parent).to eq(root_epic)
expect(level_1_epic_2.reload.parent).to eq(level_1_epic_1)
expect(level_2_epic_1.reload.parent).to eq(root_epic)
expect(level_2_subepic.reload.parent).to eq(level_2_epic_2)
expect(level_3_epic.reload.parent).to eq(level_2_epic_2)
end
it 'removes relations to epics of the old parent group' do
expect(level_2_epic_2.reload.parent).to be_nil
end
end
end
end
end
......@@ -208,6 +208,26 @@ describe VulnerabilityFeedback::CreateService, '#execute' do
end
end
context 'when feedback exists' do
let!(:feedback) { create(:vulnerability_feedback, project: project) }
let(:another_pipeline) { create(:ci_pipeline) }
let(:feedback_params) do
{
feedback_type: feedback.feedback_type, pipeline_id: another_pipeline.id, category: feedback.category,
project_fingerprint: feedback.project_fingerprint,
comment: feedback.comment,
vulnerability_data: feedback.vulnerability_data
}
end
it 'returns error when params are invalid' do
result = described_class.new(project, user, feedback_params).execute
expect(result[:status]).to eq(:error)
expect(result[:message][:pipeline]).to eq(["must associate the same project"])
end
end
context 'when params are invalid' do
context 'when vulnerability_data params is missing and feedback_type is issue' do
let(:feedback_params) do
......
......@@ -11,23 +11,50 @@ describe 'layouts/nav/sidebar/_group' do
let(:user) { create(:user) }
describe 'contribution analytics tab' do
it 'is not visible when there is no valid license and we dont show promotions' do
stub_licensed_features(contribution_analytics: false)
let!(:current_user) { create(:user) }
render
before do
group.add_guest(current_user)
expect(rendered).not_to have_text 'Contribution Analytics'
allow(view).to receive(:current_user).and_return(current_user)
end
context 'no license installed' do
let!(:cuser) { create(:admin) }
context 'contribution analytics feature is available' do
before do
stub_licensed_features(contribution_analytics: true)
end
it 'is visible' do
render
expect(rendered).to have_text 'Contribution Analytics'
end
end
context 'contribution analytics feature is not available' do
before do
stub_licensed_features(contribution_analytics: false)
end
context 'we do not show promotions' do
before do
allow(LicenseHelper).to receive(:show_promotions?).and_return(false)
end
it 'is not visible' do
render
expect(rendered).not_to have_text 'Contribution Analytics'
end
end
end
context 'no license installed' do
before do
allow(License).to receive(:current).and_return(nil)
stub_application_setting(check_namespace_plan: false)
allow(view).to receive(:can?) { |*args| Ability.allowed?(*args) }
allow(view).to receive(:current_user).and_return(cuser)
end
it 'is visible when there is no valid license but we show promotions' do
......
......@@ -4,6 +4,8 @@ module API
class Triggers < Grape::API
include PaginationParams
HTTP_GITLAB_EVENT_HEADER = "HTTP_#{WebHookService::GITLAB_EVENT_HEADER}".underscore.upcase
params do
requires :id, type: String, desc: 'The ID of a project'
end
......@@ -19,6 +21,8 @@ module API
post ":id/(ref/:ref/)trigger/pipeline", requirements: { ref: /.+/ } do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42283')
forbidden! if gitlab_pipeline_hook_request?
# validate variables
params[:variables] = params[:variables].to_h
unless params[:variables].all? { |key, value| key.is_a?(String) && value.is_a?(String) }
......@@ -128,5 +132,11 @@ module API
destroy_conditionally!(trigger)
end
end
helpers do
def gitlab_pipeline_hook_request?
request.get_header(HTTP_GITLAB_EVENT_HEADER) == WebHookService.hook_to_event(:pipeline_hooks)
end
end
end
end
# frozen_string_literal: true
# This is based on https://github.com/jch/html-pipeline/blob/v2.12.2/lib/html/pipeline/camo_filter.rb
# and Banzai::Filter::AssetProxyFilter which we use to proxy images in Markdown
module Gitlab
module AssetProxy
class << self
def proxy_url(url)
return url unless Gitlab.config.asset_proxy.enabled
return url if asset_host_whitelisted?(url)
"#{Gitlab.config.asset_proxy.url}/#{asset_url_hash(url)}/#{hexencode(url)}"
end
private
def asset_host_whitelisted?(url)
parsed_url = URI.parse(url)
Gitlab.config.asset_proxy.domain_regexp&.match?(parsed_url.host)
end
def asset_url_hash(url)
OpenSSL::HMAC.hexdigest('sha1', Gitlab.config.asset_proxy.secret_key, url)
end
def hexencode(str)
str.unpack1('H*')
end
end
end
end
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
# rubocop:disable Style/Documentation
class RecalculateProjectAuthorizationsWithMinMaxUserId
def perform(min_user_id, max_user_id)
User.where(id: min_user_id..max_user_id).find_each do |user|
service = Users::RefreshAuthorizedProjectsService.new(
user,
incorrect_auth_found_callback:
->(project_id, access_level) do
logger.info(message: 'Removing ProjectAuthorizations',
user_id: user.id,
project_id: project_id,
access_level: access_level)
end,
missing_auth_found_callback:
->(project_id, access_level) do
logger.info(message: 'Creating ProjectAuthorizations',
user_id: user.id,
project_id: project_id,
access_level: access_level)
end
)
service.execute
end
end
private
def logger
@logger ||= Gitlab::BackgroundMigration::Logger.build
end
end
end
end
......@@ -7,6 +7,8 @@ module Gitlab
GIT_INVALID_URL_REGEX = /^git\+#{URL_REGEX}/.freeze
REPO_REGEX = %r{[^/'" ]+/[^/'" ]+}.freeze
include ActionView::Helpers::SanitizeHelper
class_attribute :file_type
def self.support?(blob_name)
......@@ -62,7 +64,10 @@ module Gitlab
end
def link_tag(name, url)
%{<a href="#{ERB::Util.html_escape_once(url)}" rel="nofollow noreferrer noopener" target="_blank">#{ERB::Util.html_escape_once(name)}</a>}.html_safe
sanitize(
%{<a href="#{ERB::Util.html_escape_once(url)}" rel="nofollow noreferrer noopener" target="_blank">#{ERB::Util.html_escape_once(name)}</a>},
attributes: %w[href rel target]
)
end
# Links package names based on regex.
......
......@@ -62,6 +62,7 @@ module Gitlab
cte = Gitlab::SQL::RecursiveCTE.new(:namespaces_cte)
members = Member.arel_table
namespaces = Namespace.arel_table
group_group_links = GroupGroupLink.arel_table
# Namespaces the user is a member of.
cte << user.groups
......@@ -69,7 +70,10 @@ module Gitlab
.except(:order)
# Namespaces shared with any of the group
cte << Group.select([namespaces[:id], 'group_group_links.group_access AS access_level'])
cte << Group.select([namespaces[:id],
least(members[:access_level],
group_group_links[:group_access],
'access_level')])
.joins(join_group_group_links)
.joins(join_members_on_group_group_links)
......
......@@ -67,7 +67,13 @@ module Gitlab
return false unless can_access_git?
return false unless project
return false if !user.can?(:push_code, project) && !project.branch_allows_collaboration?(user, ref)
# Checking for an internal project to prevent an infinite loop:
# https://gitlab.com/gitlab-org/gitlab/issues/36805
if project.internal?
return false unless user.can?(:push_code, project)
else
return false if !user.can?(:push_code, project) && !project.branch_allows_collaboration?(user, ref)
end
if protected?(ProtectedBranch, project, ref)
protected_branch_accessible_to?(ref, action: :push)
......
......@@ -146,5 +146,14 @@ module Gitlab
IPAddr.new(str)
rescue IPAddr::InvalidAddressError
end
# Converts a string to an Addressable::URI object.
# If the string is not a valid URI, it returns nil.
# Param uri_string should be a String object.
# This method returns an Addressable::URI object or nil.
def parse_url(uri_string)
Addressable::URI.parse(uri_string)
rescue Addressable::URI::InvalidURIError, TypeError
end
end
end
......@@ -8386,6 +8386,9 @@ msgstr ""
msgid "False positive"
msgstr ""
msgid "Fast-forward merge is not possible. Rebase the source branch onto %{targetBranch} to allow this merge request to be merged."
msgstr ""
msgid "Fast-forward merge is not possible. Rebase the source branch onto the target branch or merge target branch into source branch to allow this merge request to be merged."
msgstr ""
......@@ -8833,7 +8836,7 @@ msgstr ""
msgid "From %{providerTitle}"
msgstr ""
msgid "From %{source_title} into"
msgid "From <code>%{source_title}</code> into"
msgstr ""
msgid "From Bitbucket"
......
......@@ -6,9 +6,13 @@ describe Groups::GroupLinksController do
let(:shared_with_group) { create(:group, :private) }
let(:shared_group) { create(:group, :private) }
let(:user) { create(:user) }
let(:group_member) { create(:user) }
let!(:project) { create(:project, group: shared_group) }
before do
sign_in(user)
shared_with_group.add_developer(group_member)
end
describe '#create' do
......@@ -40,13 +44,9 @@ describe Groups::GroupLinksController do
end
context 'when user has correct access to both groups' do
let(:group_member) { create(:user) }
before do
shared_with_group.add_developer(user)
shared_group.add_owner(user)
shared_with_group.add_developer(group_member)
end
context 'when default access level is requested' do
......@@ -56,6 +56,10 @@ describe Groups::GroupLinksController do
context 'when owner access is requested' do
let(:shared_group_access) { Gitlab::Access::OWNER }
before do
shared_with_group.add_owner(group_member)
end
include_examples 'creates group group link'
it 'allows admin access for group member' do
......@@ -64,6 +68,10 @@ describe Groups::GroupLinksController do
end
end
it 'updates project permissions' do
expect { subject }.to change { group_member.can?(:read_project, project) }.from(false).to(true)
end
context 'when shared with group id is not present' do
let(:shared_with_group_id) { nil }
......@@ -149,6 +157,7 @@ describe Groups::GroupLinksController do
context 'when user has admin access to the shared group' do
before do
shared_group.add_owner(user)
shared_with_group.refresh_members_authorized_projects
end
it 'updates existing link' do
......@@ -162,6 +171,10 @@ describe Groups::GroupLinksController do
expect(link.group_access).to eq(Gitlab::Access::GUEST)
expect(link.expires_at).to eq(expiry_date)
end
it 'updates project permissions' do
expect { subject }.to change { group_member.can?(:create_release, project) }.from(true).to(false)
end
end
context 'when user does not have admin access to the shared group' do
......@@ -199,11 +212,16 @@ describe Groups::GroupLinksController do
context 'when user has admin access to the shared group' do
before do
shared_group.add_owner(user)
shared_with_group.refresh_members_authorized_projects
end
it 'deletes existing link' do
expect { subject }.to change(GroupGroupLink, :count).by(-1)
end
it 'updates project permissions' do
expect { subject }.to change { group_member.can?(:create_release, project) }.from(true).to(false)
end
end
context 'when user does not have admin access to the shared group' do
......
......@@ -260,8 +260,10 @@ describe SnippetsController do
context 'when the snippet description contains a file' do
include FileMoverHelpers
let(:picture_file) { "/-/system/user/#{user.id}/secret56/picture.jpg" }
let(:text_file) { "/-/system/user/#{user.id}/secret78/text.txt" }
let(:picture_secret) { SecureRandom.hex }
let(:text_secret) { SecureRandom.hex }
let(:picture_file) { "/-/system/user/#{user.id}/#{picture_secret}/picture.jpg" }
let(:text_file) { "/-/system/user/#{user.id}/#{text_secret}/text.txt" }
let(:description) do
"Description with picture: ![picture](/uploads#{picture_file}) and "\
"text: [text.txt](/uploads#{text_file})"
......@@ -284,8 +286,8 @@ describe SnippetsController do
snippet = subject
expected_description = "Description with picture: "\
"![picture](/uploads/-/system/personal_snippet/#{snippet.id}/secret56/picture.jpg) and "\
"text: [text.txt](/uploads/-/system/personal_snippet/#{snippet.id}/secret78/text.txt)"
"![picture](/uploads/-/system/personal_snippet/#{snippet.id}/#{picture_secret}/picture.jpg) and "\
"text: [text.txt](/uploads/-/system/personal_snippet/#{snippet.id}/#{text_secret}/text.txt)"
expect(snippet.description).to eq(expected_description)
end
......
......@@ -5,9 +5,9 @@ require "spec_helper"
describe "User creates a merge request", :js do
include ProjectForksHelper
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
let(:title) { "Some feature" }
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
before do
project.add_maintainer(user)
......@@ -38,6 +38,26 @@ describe "User creates a merge request", :js do
end
end
context "XSS branch name exists" do
before do
project.repository.create_branch("<img/src='x'/onerror=alert('oops')>", "master")
end
it "doesn't execute the dodgy branch name" do
visit(project_new_merge_request_path(project))
find(".js-source-branch").click
click_link("<img/src='x'/onerror=alert('oops')>")
find(".js-target-branch").click
click_link("feature")
click_button("Compare branches")
expect { page.driver.browser.switch_to.alert }.to raise_error(Selenium::WebDriver::Error::NoSuchAlertError)
end
end
context "to a forked project" do
let(:forked_project) { fork_project(project, user, namespace: user.namespace, repository: true) }
......
......@@ -137,6 +137,28 @@ describe('ErrorDetails', () => {
expect(wrapper.findAll(GlButton).length).toBe(3);
});
describe('unsafe chars for culprit field', () => {
const findReportedText = () => wrapper.find('[data-qa-selector="reported_text"]');
const culprit = '<script>console.log("surprise!")</script>';
beforeEach(() => {
store.state.details.loadingStacktrace = false;
wrapper.setData({
error: {
culprit,
},
});
});
it('should not convert interpolated text to html entities', () => {
expect(findReportedText().findAll('script').length).toEqual(0);
expect(findReportedText().findAll('strong').length).toEqual(1);
});
it('should render text instead of converting to html entities', () => {
expect(findReportedText().text()).toContain(culprit);
});
});
describe('Badges', () => {
it('should show language and error level badges', () => {
wrapper.setData({
......
......@@ -5,5 +5,9 @@ require 'spec_helper'
describe GitlabSchema.types['DiffRefs'] do
it { expect(described_class.graphql_name).to eq('DiffRefs') }
it { expect(described_class).to have_graphql_fields(:base_sha, :head_sha, :start_sha) }
it { is_expected.to have_graphql_fields(:head_sha, :base_sha, :start_sha).only }
it { expect(described_class.fields['headSha'].type).to be_non_null }
it { expect(described_class.fields['baseSha'].type).not_to be_non_null }
it { expect(described_class.fields['startSha'].type).to be_non_null }
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::AssetProxy do
context 'when asset proxy is disabled' do
before do
stub_asset_proxy_setting(enabled: false)
end
it 'returns the original URL' do
url = 'http://example.com/test.png'
expect(described_class.proxy_url(url)).to eq(url)
end
end
context 'when asset proxy is enabled' do
before do
stub_asset_proxy_setting(whitelist: %w(gitlab.com *.mydomain.com))
stub_asset_proxy_setting(
enabled: true,
url: 'https://assets.example.com',
secret_key: 'shared-secret',
domain_regexp: Banzai::Filter::AssetProxyFilter.compile_whitelist(Gitlab.config.asset_proxy.whitelist)
)
end
it 'returns a proxied URL' do
url = 'http://example.com/test.png'
proxied_url = 'https://assets.example.com/08df250eeeef1a8cf2c761475ac74c5065105612/687474703a2f2f6578616d706c652e636f6d2f746573742e706e67'
expect(described_class.proxy_url(url)).to eq(proxied_url)
end
context 'whitelisted domain' do
it 'returns original URL for single domain whitelist' do
url = 'http://gitlab.com/test.png'
expect(described_class.proxy_url(url)).to eq(url)
end
it 'returns original URL for wildcard subdomain whitelist' do
url = 'http://test.mydomain.com/test.png'
expect(described_class.proxy_url(url)).to eq(url)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::BackgroundMigration::RecalculateProjectAuthorizationsWithMinMaxUserId, :migration, schema: 20200204113224 do
let(:users_table) { table(:users) }
let(:min) { 1 }
let(:max) { 5 }
before do
min.upto(max) do |i|
users_table.create!(id: i, email: "user#{i}@example.com", projects_limit: 10)
end
end
describe '#perform' do
it 'initializes Users::RefreshAuthorizedProjectsService with correct users' do
min.upto(max) do |i|
user = User.find(i)
expect(Users::RefreshAuthorizedProjectsService).to(
receive(:new).with(user, any_args).and_call_original)
end
described_class.new.perform(min, max)
end
it 'executes Users::RefreshAuthorizedProjectsService' do
expected_call_counts = max - min + 1
service = instance_double(Users::RefreshAuthorizedProjectsService)
expect(Users::RefreshAuthorizedProjectsService).to(
receive(:new).exactly(expected_call_counts).times.and_return(service))
expect(service).to receive(:execute).exactly(expected_call_counts).times
described_class.new.perform(min, max)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::DependencyLinker::BaseLinker do
let(:linker_class) do
Class.new(described_class) do
def link_dependencies
link_regex(%r{^(?<name>https?://[^ ]+)}, &:itself)
end
end
end
let(:plain_content) do
<<~CONTENT
http://\\njavascript:alert(1)
https://gitlab.com/gitlab-org/gitlab
CONTENT
end
let(:highlighted_content) do
<<~CONTENT
<span><span>http://</span><span>\\n</span><span>javascript:alert(1)</span></span>
<span><span>https://gitlab.com/gitlab-org/gitlab</span></span>
CONTENT
end
let(:linker) { linker_class.new(plain_content, highlighted_content) }
describe '#link' do
subject { linker.link }
it 'only converts valid links' do
expect(subject).to eq(
<<~CONTENT
<span><span>#{link('http://')}</span><span>#{link('\n', url: '%5Cn')}</span><span>#{link('javascript:alert(1)', url: nil)}</span></span>
<span><span>#{link('https://gitlab.com/gitlab-org/gitlab')}</span></span>
CONTENT
)
end
end
def link(text, url: text)
attrs = [
'rel="nofollow noreferrer noopener"',
'target="_blank"'
]
attrs.unshift(%{href="#{url}"}) if url
%{<a #{attrs.join(' ')}>#{text}</a>}
end
end
......@@ -109,6 +109,20 @@ describe Gitlab::ProjectAuthorizations do
end
end
context 'with lower group access level than max access level for share' do
let(:user) { create(:user) }
it 'creates proper authorizations' do
group.add_reporter(user)
mapping = map_access_levels(authorizations)
expect(mapping[project_parent.id]).to be_nil
expect(mapping[project.id]).to eq(Gitlab::Access::REPORTER)
expect(mapping[project_child.id]).to eq(Gitlab::Access::REPORTER)
end
end
context 'parent group user' do
let(:user) { parent_group_user }
......
......@@ -30,6 +30,17 @@ describe Gitlab::UserAccess do
end
end
describe 'push to branch in an internal project' do
it 'will not infinitely loop when a project is internal' do
project.visibility_level = Gitlab::VisibilityLevel::INTERNAL
project.save!
expect(project).not_to receive(:branch_allows_collaboration?)
access.can_push_to_branch?('master')
end
end
describe 'push to empty project' do
let(:empty_project) { create(:project_empty_repo) }
let(:project_access) { described_class.new(user, project: empty_project) }
......
......@@ -291,4 +291,18 @@ describe Gitlab::Utils do
expect(described_class.string_to_ip_object('1:0:0:0:0:0:0:0/124')).to eq(IPAddr.new('1:0:0:0:0:0:0:0/124'))
end
end
describe '.parse_url' do
it 'returns Addressable::URI object' do
expect(described_class.parse_url('http://gitlab.com')).to be_instance_of(Addressable::URI)
end
it 'returns nil when URI cannot be parsed' do
expect(described_class.parse_url('://gitlab.com')).to be nil
end
it 'returns nil with invalid parameter' do
expect(described_class.parse_url(1)).to be nil
end
end
end
# frozen_string_literal: true
require 'spec_helper'
require Rails.root.join('db', 'migrate', '20200214085940_clean_grafana_url.rb')
describe CleanGrafanaUrl, :migration do
let(:application_settings_table) { table(:application_settings) }
[
'javascript:alert(window.opener.document.location)',
' javascript:alert(window.opener.document.location)'
].each do |grafana_url|
it "sets grafana_url back to its default value when grafana_url is '#{grafana_url}'" do
application_settings = application_settings_table.create!(grafana_url: grafana_url)
migrate!
expect(application_settings.reload.grafana_url).to eq('/-/grafana')
end
end
['/-/grafana', '/some/relative/url', 'http://localhost:9000'].each do |grafana_url|
it "does not modify grafana_url when grafana_url is '#{grafana_url}'" do
application_settings = application_settings_table.create!(grafana_url: grafana_url)
migrate!
expect(application_settings.reload.grafana_url).to eq(grafana_url)
end
end
context 'when application_settings table has no rows' do
it 'does not fail' do
migrate!
end
end
end
# frozen_string_literal: true
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20200204113224_schedule_recalculate_project_authorizations_second_run.rb')
describe ScheduleRecalculateProjectAuthorizationsSecondRun, :migration do
let(:users_table) { table(:users) }
before do
stub_const("#{described_class}::BATCH_SIZE", 2)
1.upto(4) do |i|
users_table.create!(id: i, name: "user#{i}", email: "user#{i}@example.com", projects_limit: 1)
end
end
it 'schedules background migration' do
Sidekiq::Testing.fake! do
Timecop.freeze do
migrate!
expect(BackgroundMigrationWorker.jobs.size).to eq(2)
expect(described_class::MIGRATION).to be_scheduled_migration(1, 2)
expect(described_class::MIGRATION).to be_scheduled_migration(3, 4)
end
end
end
end
......@@ -19,6 +19,7 @@ describe ApplicationSetting do
let(:http) { 'http://example.com' }
let(:https) { 'https://example.com' }
let(:ftp) { 'ftp://example.com' }
let(:javascript) { 'javascript:alert(window.opener.document.location)' }
it { is_expected.to allow_value(nil).for(:home_page_url) }
it { is_expected.to allow_value(http).for(:home_page_url) }
......@@ -81,6 +82,53 @@ describe ApplicationSetting do
it { is_expected.not_to allow_value('abc').for(:minimum_password_length) }
it { is_expected.to allow_value(10).for(:minimum_password_length) }
context 'grafana_url validations' do
before do
subject.instance_variable_set(:@parsed_grafana_url, nil)
end
it { is_expected.to allow_value(http).for(:grafana_url) }
it { is_expected.to allow_value(https).for(:grafana_url) }
it { is_expected.not_to allow_value(ftp).for(:grafana_url) }
it { is_expected.not_to allow_value(javascript).for(:grafana_url) }
it { is_expected.to allow_value('/-/grafana').for(:grafana_url) }
it { is_expected.to allow_value('http://localhost:9000').for(:grafana_url) }
context 'when local URLs are not allowed in system hooks' do
before do
stub_application_setting(allow_local_requests_from_system_hooks: false)
end
it { is_expected.not_to allow_value('http://localhost:9000').for(:grafana_url) }
end
context 'with invalid grafana URL' do
it 'adds an error' do
subject.grafana_url = ' ' + http
expect(subject.save).to be false
expect(subject.errors[:grafana_url]).to eq([
'must be a valid relative or absolute URL. ' \
'Please check your Grafana URL setting in ' \
'Admin Area > Settings > Metrics and profiling > Metrics - Grafana'
])
end
end
context 'with blocked grafana URL' do
it 'adds an error' do
subject.grafana_url = javascript
expect(subject.save).to be false
expect(subject.errors[:grafana_url]).to eq([
'is blocked: Only allowed schemes are http, https. Please check your ' \
'Grafana URL setting in ' \
'Admin Area > Settings > Metrics and profiling > Metrics - Grafana'
])
end
end
end
context 'when snowplow is enabled' do
before do
setting.snowplow_enabled = true
......
......@@ -91,6 +91,22 @@ describe Badge do
let(:method) { :image_url }
it_behaves_like 'rendered_links'
context 'when asset proxy is enabled' do
let(:placeholder_url) { 'http://www.example.com/image' }
before do
stub_asset_proxy_setting(
enabled: true,
url: 'https://assets.example.com',
secret_key: 'shared-secret'
)
end
it 'returns a proxied URL' do
expect(badge.rendered_image_url).to start_with('https://assets.example.com')
end
end
end
end
end
......@@ -563,6 +563,18 @@ describe Group do
expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::DEVELOPER)
expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::DEVELOPER)
end
context 'with lower group access level than max access level for share' do
let(:user) { create(:user) }
it 'returns correct access level' do
group.add_reporter(user)
expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::REPORTER)
expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::REPORTER)
end
end
end
context 'with user in the parent group' do
......@@ -584,6 +596,33 @@ describe Group do
expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
end
end
context 'unrelated project owner' do
let(:common_id) { [Project.maximum(:id).to_i, Namespace.maximum(:id).to_i].max + 999 }
let!(:group) { create(:group, id: common_id) }
let!(:unrelated_project) { create(:project, id: common_id) }
let(:user) { unrelated_project.owner }
it 'returns correct access level' do
expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
end
end
context 'user without accepted access request' do
let!(:user) { create(:user) }
before do
create(:group_member, :developer, :access_request, user: user, group: group)
end
it 'returns correct access level' do
expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
end
end
end
context 'when feature flag share_group_with_group is disabled' do
......
......@@ -65,10 +65,10 @@ describe GroupMember do
end
describe '#update_two_factor_requirement' do
let(:user) { build :user }
let(:group_member) { build :group_member, user: user }
it 'is called after creation and deletion' do
user = build :user
group_member = build :group_member, user: user
expect(user).to receive(:update_two_factor_requirement)
group_member.save
......@@ -79,6 +79,21 @@ describe GroupMember do
end
end
describe '#after_accept_invite' do
it 'calls #update_two_factor_requirement' do
email = 'foo@email.com'
user = build(:user, email: email)
group = create(:group, require_two_factor_authentication: true)
group_member = create(:group_member, group: group, invite_token: '1234', invite_email: email)
expect(user).to receive(:require_two_factor_authentication_from_group).and_call_original
group_member.accept_invite!(user)
expect(user.require_two_factor_authentication_from_group).to be_truthy
end
end
context 'access levels' do
context 'with parent group' do
it_behaves_like 'inherited access level as a member of entity' do
......
......@@ -4866,6 +4866,38 @@ describe Project do
end
end
context 'with cross internal project merge requests' do
let(:project) { create(:project, :repository, :internal) }
let(:forked_project) { fork_project(project, nil, repository: true) }
let(:user) { double(:user) }
it "does not endlessly loop for internal projects with MRs to each other", :sidekiq_inline do
allow(user).to receive(:can?).and_return(true, false, true)
allow(user).to receive(:id).and_return(1)
create(
:merge_request,
target_project: project,
target_branch: 'merge-test',
source_project: forked_project,
source_branch: 'merge-test',
allow_collaboration: true
)
create(
:merge_request,
target_project: forked_project,
target_branch: 'merge-test',
source_project: project,
source_branch: 'merge-test',
allow_collaboration: true
)
expect(user).to receive(:can?).at_most(5).times
project.branch_allows_collaboration?(user, "merge-test")
end
end
context 'with cross project merge requests' do
let(:user) { create(:user) }
let(:target_project) { create(:project, :repository) }
......
......@@ -6,6 +6,7 @@ describe Ci::PipelinePresenter do
include Gitlab::Routing
let(:user) { create(:user) }
let(:current_user) { user }
let(:project) { create(:project) }
let(:pipeline) { create(:ci_pipeline, project: project) }
......@@ -15,7 +16,7 @@ describe Ci::PipelinePresenter do
before do
project.add_developer(user)
allow(presenter).to receive(:current_user) { user }
allow(presenter).to receive(:current_user) { current_user }
end
it 'inherits from Gitlab::View::Presenter::Delegated' do
......@@ -224,10 +225,90 @@ describe Ci::PipelinePresenter do
describe '#all_related_merge_requests' do
it 'memoizes the returned relation' do
query_count = ActiveRecord::QueryRecorder.new do
2.times { presenter.send(:all_related_merge_requests).count }
3.times { presenter.send(:all_related_merge_requests).count }
end.count
expect(query_count).to eq(1)
expect(query_count).to eq(2)
end
context 'permissions' do
let!(:merge_request) do
create(:merge_request, project: project, source_project: project)
end
subject(:all_related_merge_requests) do
presenter.send(:all_related_merge_requests)
end
shared_examples 'private merge requests' do
context 'when not logged in' do
let(:current_user) {}
it { is_expected.to be_empty }
end
context 'when logged in as a non_member' do
let(:current_user) { create(:user) }
it { is_expected.to be_empty }
end
context 'when logged in as a guest' do
let(:current_user) { create(:user) }
before do
project.add_guest(current_user)
end
it { is_expected.to be_empty }
end
context 'when logged in as a developer' do
it { is_expected.to contain_exactly(merge_request) }
end
context 'when logged in as a maintainer' do
let(:current_user) { create(:user) }
before do
project.add_maintainer(current_user)
end
it { is_expected.to contain_exactly(merge_request) }
end
end
context 'with a private project' do
it_behaves_like 'private merge requests'
end
context 'with a public project with private merge requests' do
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
project
.project_feature
.update!(merge_requests_access_level: ProjectFeature::PRIVATE)
end
it_behaves_like 'private merge requests'
end
context 'with a public project with public merge requests' do
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
project
.project_feature
.update!(merge_requests_access_level: ProjectFeature::ENABLED)
end
context 'when not logged in' do
let(:current_user) {}
it { is_expected.to contain_exactly(merge_request) }
end
end
end
end
......
......@@ -116,6 +116,18 @@ describe API::Triggers do
end
end
end
context 'when is triggered by a pipeline hook' do
it 'does not create a new pipeline' do
expect do
post api("/projects/#{project.id}/ref/master/trigger/pipeline?token=#{trigger_token}"),
params: { ref: 'refs/heads/other-branch' },
headers: { WebHookService::GITLAB_EVENT_HEADER => 'Pipeline Hook' }
end.not_to change(Ci::Pipeline, :count)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
describe 'GET /projects/:id/triggers' do
......
......@@ -769,6 +769,15 @@ describe Auth::ContainerRegistryAuthenticationService do
context 'when deploy token has read_registry as a scope' do
let(:current_user) { create(:deploy_token, projects: [project]) }
shared_examples 'able to login' do
context 'registry provides read_container_image authentication_abilities' do
let(:current_params) { {} }
let(:authentication_abilities) { [:read_container_image] }
it_behaves_like 'an authenticated'
end
end
context 'for public project' do
let(:project) { create(:project, :public) }
......@@ -783,6 +792,8 @@ describe Auth::ContainerRegistryAuthenticationService do
it_behaves_like 'an inaccessible'
end
it_behaves_like 'able to login'
end
context 'for internal project' do
......@@ -799,6 +810,8 @@ describe Auth::ContainerRegistryAuthenticationService do
it_behaves_like 'an inaccessible'
end
it_behaves_like 'able to login'
end
context 'for private project' do
......@@ -815,18 +828,38 @@ describe Auth::ContainerRegistryAuthenticationService do
it_behaves_like 'an inaccessible'
end
it_behaves_like 'able to login'
end
end
context 'when deploy token does not have read_registry scope' do
let(:current_user) { create(:deploy_token, projects: [project], read_registry: false) }
shared_examples 'unable to login' do
context 'registry provides no container authentication_abilities' do
let(:current_params) { {} }
let(:authentication_abilities) { [] }
it_behaves_like 'a forbidden'
end
context 'registry provides inapplicable container authentication_abilities' do
let(:current_params) { {} }
let(:authentication_abilities) { [:download_code] }
it_behaves_like 'a forbidden'
end
end
context 'for public project' do
let(:project) { create(:project, :public) }
context 'when pulling' do
it_behaves_like 'a pullable'
end
it_behaves_like 'unable to login'
end
context 'for internal project' do
......@@ -835,6 +868,8 @@ describe Auth::ContainerRegistryAuthenticationService do
context 'when pulling' do
it_behaves_like 'an inaccessible'
end
it_behaves_like 'unable to login'
end
context 'for private project' do
......@@ -843,6 +878,15 @@ describe Auth::ContainerRegistryAuthenticationService do
context 'when pulling' do
it_behaves_like 'an inaccessible'
end
context 'when logging in' do
let(:current_params) { {} }
let(:authentication_abilities) { [] }
it_behaves_like 'a forbidden'
end
it_behaves_like 'unable to login'
end
end
......
......@@ -40,24 +40,11 @@ describe Groups::GroupLinks::DestroyService, '#execute' do
end
it 'updates project authorization once per group' do
expect(GroupGroupLink).to receive(:delete)
expect(GroupGroupLink).to receive(:delete).and_call_original
expect(group).to receive(:refresh_members_authorized_projects).once
expect(another_group).to receive(:refresh_members_authorized_projects).once
subject.execute(links)
end
it 'rolls back changes when error happens' do
group.add_developer(user)
expect(group).to receive(:refresh_members_authorized_projects).once.and_call_original
expect(another_group).to(
receive(:refresh_members_authorized_projects).and_raise('boom'))
expect { subject.execute(links) }.to raise_error('boom')
expect(GroupGroupLink.count).to eq(links.length)
expect(Ability.allowed?(user, :read_project, project)).to be_truthy
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Groups::GroupLinks::UpdateService, '#execute' do
let(:user) { create(:user) }
let_it_be(:group) { create(:group, :private) }
let_it_be(:shared_group) { create(:group, :private) }
let_it_be(:project) { create(:project, group: shared_group) }
let(:group_member) { create(:user) }
let!(:link) { create(:group_group_link, shared_group: shared_group, shared_with_group: group) }
let(:expiry_date) { 1.month.from_now.to_date }
let(:group_link_params) do
{ group_access: Gitlab::Access::GUEST,
expires_at: expiry_date }
end
subject { described_class.new(link).execute(group_link_params) }
before do
group.add_developer(group_member)
end
it 'updates existing link' do
expect(link.group_access).to eq(Gitlab::Access::DEVELOPER)
expect(link.expires_at).to be_nil
subject
link.reload
expect(link.group_access).to eq(Gitlab::Access::GUEST)
expect(link.expires_at).to eq(expiry_date)
end
it 'updates project permissions' do
expect { subject }.to change { group_member.can?(:create_release, project) }.from(true).to(false)
end
it 'executes UserProjectAccessChangedService' do
expect_next_instance_of(UserProjectAccessChangedService) do |service|
expect(service).to receive(:execute)
end
subject
end
context 'with only param not requiring authorization refresh' do
let(:group_link_params) { { expires_at: Date.tomorrow } }
it 'does not execute UserProjectAccessChangedService' do
expect(UserProjectAccessChangedService).not_to receive(:new)
subject
end
end
end
......@@ -134,6 +134,21 @@ describe Projects::LfsPointers::LfsDownloadService do
end
end
context 'when an lfs object with the same oid already exists' do
let!(:existing_lfs_object) { create(:lfs_object, oid: oid) }
before do
stub_full_request(download_link).to_return(body: lfs_content)
end
it_behaves_like 'no lfs object is created'
it 'does not update the file attached to the existing LfsObject' do
expect { subject.execute }
.not_to change { existing_lfs_object.reload.file.file.file }
end
end
context 'when credentials present' do
let(:download_link_with_credentials) { "http://user:password@gitlab.com/#{oid}" }
let(:lfs_object) { LfsDownloadObject.new(oid: oid, size: size, link: download_link_with_credentials) }
......@@ -211,17 +226,5 @@ describe Projects::LfsPointers::LfsDownloadService do
subject.execute
end
end
context 'when an lfs object with the same oid already exists' do
before do
create(:lfs_object, oid: oid)
end
it 'does not download the file' do
expect(subject).not_to receive(:download_lfs_file!)
subject.execute
end
end
end
end
......@@ -24,7 +24,6 @@ describe Projects::LfsPointers::LfsObjectDownloadListService do
describe '#execute' do
context 'when no lfs pointer is linked' do
before do
allow_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute).and_return([])
allow_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).and_return(oid_download_links)
expect(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:new).with(project, remote_uri: URI.parse(default_endpoint)).and_call_original
end
......@@ -35,12 +34,6 @@ describe Projects::LfsPointers::LfsObjectDownloadListService do
subject.execute
end
it 'links existent lfs objects to the project' do
expect_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute)
subject.execute
end
it 'retrieves the download links of non existent objects' do
expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).with(all_oids)
......@@ -48,32 +41,6 @@ describe Projects::LfsPointers::LfsObjectDownloadListService do
end
end
context 'when some lfs objects are linked' do
before do
allow_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute).and_return(existing_lfs_objects.keys)
allow_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).and_return(oid_download_links)
end
it 'retrieves the download links of non existent objects' do
expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).with(oids)
subject.execute
end
end
context 'when all lfs objects are linked' do
before do
allow_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute).and_return(all_oids.keys)
allow_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute)
end
it 'retrieves no download links' do
expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).with({}).and_call_original
expect(subject.execute).to be_empty
end
end
context 'when lfsconfig file exists' do
before do
allow(project.repository).to receive(:lfsconfig_for).and_return("[lfs]\n\turl = #{lfs_endpoint}\n")
......
......@@ -7,6 +7,7 @@ RSpec.shared_context 'GroupPolicy context' do
let_it_be(:maintainer) { create(:user) }
let_it_be(:owner) { create(:user) }
let_it_be(:admin) { create(:admin) }
let_it_be(:non_group_member) { create(:user) }
let_it_be(:group, refind: true) { create(:group, :private, :owner_subgroup_creation_only) }
let(:guest_permissions) do
......
......@@ -69,13 +69,39 @@ RSpec.shared_examples 'handle uploads' do
end
describe "GET #show" do
let(:filename) { "rails_sample.jpg" }
let(:upload_service) do
UploadService.new(model, jpg, uploader_class).execute
end
let(:show_upload) do
get :show, params: params.merge(secret: secret, filename: "rails_sample.jpg")
get :show, params: params.merge(secret: secret, filename: filename)
end
before do
allow(FileUploader).to receive(:generate_secret).and_return(secret)
UploadService.new(model, jpg, uploader_class).execute
upload_service
end
context 'when the secret is invalid' do
let(:secret) { "../../../../../../../../" }
let(:filename) { "Gemfile.lock" }
let(:upload_service) { nil }
it 'responds with status 404' do
show_upload
expect(response).to have_gitlab_http_status(:not_found)
end
it 'is a working exploit without the validation' do
allow_any_instance_of(FileUploader).to receive(:secret) { secret }
show_upload
expect(response).to have_gitlab_http_status(:ok)
end
end
context 'when accessing a specific upload via different model' do
......
......@@ -7,7 +7,7 @@ describe FileMover do
let(:user) { create(:user) }
let(:filename) { 'banana_sample.gif' }
let(:secret) { 'secret55' }
let(:secret) { SecureRandom.hex }
let(:temp_file_path) { File.join("uploads/-/system/user/#{user.id}", secret, filename) }
let(:temp_description) do
......@@ -47,8 +47,8 @@ describe FileMover do
subject
expect(snippet.reload.description)
.to eq("test ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/secret55/banana_sample.gif) "\
"same ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/secret55/banana_sample.gif) ")
.to eq("test ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/#{secret}/banana_sample.gif) "\
"same ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/#{secret}/banana_sample.gif) ")
end
it 'updates existing upload record' do
......@@ -75,8 +75,8 @@ describe FileMover do
subject
expect(snippet.reload.description)
.to eq("test ![banana_sample](/uploads/-/system/user/#{user.id}/secret55/banana_sample.gif) "\
"same ![banana_sample](/uploads/-/system/user/#{user.id}/secret55/banana_sample.gif) ")
.to eq("test ![banana_sample](/uploads/-/system/user/#{user.id}/#{secret}/banana_sample.gif) "\
"same ![banana_sample](/uploads/-/system/user/#{user.id}/#{secret}/banana_sample.gif) ")
end
it 'does not change the upload record' do
......@@ -101,8 +101,8 @@ describe FileMover do
subject
expect(snippet.reload.description)
.to eq("test ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/secret55/banana_sample.gif) "\
"same ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/secret55/banana_sample.gif) ")
.to eq("test ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/#{secret}/banana_sample.gif) "\
"same ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/#{secret}/banana_sample.gif) ")
end
it 'creates new target upload record an delete the old upload' do
......@@ -121,8 +121,8 @@ describe FileMover do
subject
expect(snippet.reload.description)
.to eq("test ![banana_sample](/uploads/-/system/user/#{user.id}/secret55/banana_sample.gif) "\
"same ![banana_sample](/uploads/-/system/user/#{user.id}/secret55/banana_sample.gif) ")
.to eq("test ![banana_sample](/uploads/-/system/user/#{user.id}/#{secret}/banana_sample.gif) "\
"same ![banana_sample](/uploads/-/system/user/#{user.id}/#{secret}/banana_sample.gif) ")
end
it 'does not change the upload record' do
......@@ -138,12 +138,8 @@ describe FileMover do
let(:temp_file_path) { File.join("uploads/-/system/user/#{user.id}", '..', 'another_subdir_of_temp') }
it 'does not trigger move if path is outside designated directory' do
stub_file_mover("uploads/-/system/user/#{user.id}/another_subdir_of_temp")
expect(FileUtils).not_to receive(:move)
subject
expect(snippet.reload.description).to eq(temp_description)
expect { subject }.to raise_error(FileUploader::InvalidSecret)
end
end
......
......@@ -6,7 +6,8 @@ describe FileUploader do
let(:group) { create(:group, name: 'awesome') }
let(:project) { create(:project, :legacy_storage, namespace: group, name: 'project') }
let(:uploader) { described_class.new(project, :avatar) }
let(:upload) { double(model: project, path: 'secret/foo.jpg') }
let(:upload) { double(model: project, path: "#{secret}/foo.jpg") }
let(:secret) { "55dc16aa0edd05693fd98b5051e83321" } # this would be nicer as SecureRandom.hex, but the shared_examples breaks
subject { uploader }
......@@ -14,7 +15,7 @@ describe FileUploader do
include_examples 'builds correct paths',
store_dir: %r{awesome/project/\h+},
upload_path: %r{\h+/<filename>},
absolute_path: %r{#{described_class.root}/awesome/project/secret/foo.jpg}
absolute_path: %r{#{described_class.root}/awesome/project/55dc16aa0edd05693fd98b5051e83321/foo.jpg}
end
context 'legacy storage' do
......@@ -51,11 +52,11 @@ describe FileUploader do
end
describe 'initialize' do
let(:uploader) { described_class.new(double, secret: 'secret') }
let(:uploader) { described_class.new(double, secret: secret) }
it 'accepts a secret parameter' do
expect(described_class).not_to receive(:generate_secret)
expect(uploader.secret).to eq('secret')
expect(uploader.secret).to eq(secret)
end
end
......@@ -154,8 +155,39 @@ describe FileUploader do
describe '#secret' do
it 'generates a secret if none is provided' do
expect(described_class).to receive(:generate_secret).and_return('secret')
expect(uploader.secret).to eq('secret')
expect(described_class).to receive(:generate_secret).and_return(secret)
expect(uploader.secret).to eq(secret)
expect(uploader.secret.size).to eq(32)
end
context "validation" do
before do
uploader.instance_variable_set(:@secret, secret)
end
context "32-byte hexadecimal" do
let(:secret) { SecureRandom.hex }
it "returns the secret" do
expect(uploader.secret).to eq(secret)
end
end
context "10-byte hexadecimal" do
let(:secret) { SecureRandom.hex(10) }
it "returns the secret" do
expect(uploader.secret).to eq(secret)
end
end
context "invalid secret supplied" do
let(:secret) { "%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2Fgrafana%2Fconf%2F" }
it "raises an exception" do
expect { uploader.secret }.to raise_error(described_class::InvalidSecret)
end
end
end
end
......
......@@ -6,12 +6,13 @@ describe PersonalFileUploader do
let(:model) { create(:personal_snippet) }
let(:uploader) { described_class.new(model) }
let(:upload) { create(:upload, :personal_snippet_upload) }
let(:secret) { SecureRandom.hex }
subject { uploader }
shared_examples '#base_dir' do
before do
subject.instance_variable_set(:@secret, 'secret')
subject.instance_variable_set(:@secret, secret)
end
it 'is prefixed with uploads/-/system' do
......@@ -23,12 +24,12 @@ describe PersonalFileUploader do
shared_examples '#to_h' do
before do
subject.instance_variable_set(:@secret, 'secret')
subject.instance_variable_set(:@secret, secret)
end
it 'is correct' do
allow(uploader).to receive(:file).and_return(double(extension: 'txt', filename: 'file_name'))
expected_url = "/uploads/-/system/personal_snippet/#{model.id}/secret/file_name"
expected_url = "/uploads/-/system/personal_snippet/#{model.id}/#{secret}/file_name"
expect(uploader.to_h).to eq(
alt: 'file_name',
......
......@@ -5,6 +5,9 @@ require 'spec_helper'
describe AddressableUrlValidator do
let!(:badge) { build(:badge, link_url: 'http://www.example.com') }
let(:validator) { described_class.new(validator_options.reverse_merge(attributes: [:link_url])) }
let(:validator_options) { {} }
subject { validator.validate(badge) }
include_examples 'url validator examples', described_class::DEFAULT_OPTIONS[:schemes]
......@@ -114,6 +117,19 @@ describe AddressableUrlValidator do
end
end
context 'when blocked_message is set' do
let(:message) { 'is not allowed due to: %{exception_message}' }
let(:validator_options) { { blocked_message: message } }
it 'blocks url with provided error message' do
badge.link_url = 'javascript:alert(window.opener.document.location)'
subject
expect(badge.errors.first[1]).to eq 'is not allowed due to: Only allowed schemes are http, https'
end
end
context 'when allow_nil is set to true' do
let(:validator) { described_class.new(attributes: [:link_url], allow_nil: true) }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment