summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-03-04 21:07:54 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-03-04 21:07:54 +0000
commit2fd92f2dc784ade9cb4e1c33dd60cbfad7b86818 (patch)
tree7779f36689db97a46e0268a4aec1d49f283eb0c8
parent42ca24aa5bbab7a2d43bc866d9bee9876941cea2 (diff)
downloadgitlab-ce-2fd92f2dc784ade9cb4e1c33dd60cbfad7b86818.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/ci/review.gitlab-ci.yml1
-rw-r--r--CHANGELOG-EE.md11
-rw-r--r--app/assets/javascripts/error_tracking/components/error_details.vue25
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue6
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss14
-rw-r--r--app/controllers/concerns/uploads_actions.rb1
-rw-r--r--app/controllers/groups/group_links_controller.rb2
-rw-r--r--app/graphql/types/diff_refs_type.rb2
-rw-r--r--app/models/application_setting.rb30
-rw-r--r--app/models/badge.rb4
-rw-r--r--app/models/group.rb11
-rw-r--r--app/models/members/group_member.rb1
-rw-r--r--app/models/user_detail.rb2
-rw-r--r--app/presenters/ci/pipeline_presenter.rb6
-rw-r--r--app/services/auth/container_registry_authentication_service.rb18
-rw-r--r--app/services/groups/group_links/destroy_service.rb14
-rw-r--r--app/services/groups/group_links/update_service.rb29
-rw-r--r--app/services/projects/lfs_pointers/lfs_download_service.rb19
-rw-r--r--app/services/projects/lfs_pointers/lfs_object_download_list_service.rb12
-rw-r--r--app/services/web_hook_service.rb8
-rw-r--r--app/uploaders/file_uploader.rb7
-rw-r--r--app/validators/addressable_url_validator.rb8
-rw-r--r--app/views/admin/application_settings/_grafana.html.haml2
-rw-r--r--app/views/clusters/clusters/_form.html.haml6
-rw-r--r--app/views/clusters/clusters/user/_form.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_admin.html.haml2
-rw-r--r--app/views/shared/issuable/form/_branch_chooser.html.haml4
-rw-r--r--changelogs/unreleased/208675-add-package_name-as-option-to-packages-api.yml5
-rw-r--r--changelogs/unreleased/georgekoltsov-27883-fix-import-pipeline-order.yml5
-rw-r--r--config/initializers/8_devise.rb12
-rw-r--r--config/initializers/sidekiq.rb18
-rw-r--r--db/migrate/20200214085940_clean_grafana_url.rb22
-rw-r--r--db/post_migrate/20200204113224_schedule_recalculate_project_authorizations_second_run.rb32
-rw-r--r--doc/administration/high_availability/README.md20
-rw-r--r--doc/administration/monitoring/performance/grafana_configuration.md5
-rw-r--r--doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md41
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql2
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json10
-rw-r--r--doc/api/graphql/reference/index.md2
-rw-r--r--doc/api/packages.md1
-rw-r--r--doc/development/testing_guide/review_apps.md33
-rw-r--r--doc/user/project/merge_requests/code_quality.md17
-rw-r--r--lib/api/triggers.rb10
-rw-r--r--lib/gitlab/asset_proxy.rb33
-rw-r--r--lib/gitlab/background_migration/recalculate_project_authorizations_with_min_max_user_id.rb38
-rw-r--r--lib/gitlab/dependency_linker/base_linker.rb7
-rw-r--r--lib/gitlab/import_export/relation_tree_restorer.rb5
-rw-r--r--lib/gitlab/project_authorizations.rb6
-rw-r--r--lib/gitlab/sidekiq_logging/json_formatter.rb3
-rw-r--r--lib/gitlab/user_access.rb8
-rw-r--r--lib/gitlab/utils.rb9
-rw-r--r--locale/gitlab.pot22
-rw-r--r--qa/qa.rb2
-rw-r--r--qa/qa/page/project/operations/kubernetes/add_existing.rb4
-rw-r--r--qa/qa/page/project/operations/kubernetes/show.rb16
-rw-r--r--qa/qa/service/cluster_provider/k3s.rb94
-rw-r--r--qa/qa/service/docker_run/base.rb4
-rw-r--r--qa/qa/service/docker_run/k3s.rb46
-rw-r--r--qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb17
-rw-r--r--qa/spec/service/docker_run/k3s_spec.rb32
-rwxr-xr-xscripts/review_apps/automated_cleanup.rb16
-rw-r--r--spec/controllers/groups/group_links_controller_spec.rb26
-rw-r--r--spec/controllers/snippets_controller_spec.rb10
-rw-r--r--spec/features/issues/issue_detail_spec.rb2
-rw-r--r--spec/features/merge_request/user_creates_merge_request_spec.rb24
-rw-r--r--spec/features/projects/wiki/user_updates_wiki_page_spec.rb7
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/project.json6
-rw-r--r--spec/frontend/error_tracking/components/error_details_spec.js22
-rw-r--r--spec/graphql/types/diff_refs_type_spec.rb6
-rw-r--r--spec/lib/gitlab/asset_proxy_spec.rb50
-rw-r--r--spec/lib/gitlab/background_migration/recalculate_project_authorizations_with_min_max_user_id_spec.rb38
-rw-r--r--spec/lib/gitlab/dependency_linker/base_linker_spec.rb53
-rw-r--r--spec/lib/gitlab/import_export/project/tree_restorer_spec.rb22
-rw-r--r--spec/lib/gitlab/project_authorizations_spec.rb14
-rw-r--r--spec/lib/gitlab/user_access_spec.rb11
-rw-r--r--spec/lib/gitlab/utils_spec.rb14
-rw-r--r--spec/migrations/clean_grafana_url_spec.rb37
-rw-r--r--spec/migrations/schedule_recalculate_project_authorizations_second_run_spec.rb28
-rw-r--r--spec/models/application_setting_spec.rb48
-rw-r--r--spec/models/badge_spec.rb16
-rw-r--r--spec/models/group_spec.rb39
-rw-r--r--spec/models/members/group_member_spec.rb21
-rw-r--r--spec/models/project_spec.rb32
-rw-r--r--spec/models/user_detail_spec.rb2
-rw-r--r--spec/presenters/ci/pipeline_presenter_spec.rb87
-rw-r--r--spec/requests/api/triggers_spec.rb12
-rw-r--r--spec/services/auth/container_registry_authentication_service_spec.rb44
-rw-r--r--spec/services/groups/group_links/destroy_service_spec.rb15
-rw-r--r--spec/services/groups/group_links/update_service_spec.rb59
-rw-r--r--spec/services/projects/lfs_pointers/lfs_download_service_spec.rb27
-rw-r--r--spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb33
-rw-r--r--spec/support/shared_contexts/policies/group_policy_shared_context.rb1
-rw-r--r--spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb30
-rw-r--r--spec/uploaders/file_mover_spec.rb24
-rw-r--r--spec/uploaders/file_uploader_spec.rb44
-rw-r--r--spec/uploaders/personal_file_uploader_spec.rb7
-rw-r--r--spec/validators/addressable_url_validator_spec.rb16
97 files changed, 1491 insertions, 280 deletions
diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml
index 2fafc1f1f50..12c1507da62 100644
--- a/.gitlab/ci/review.gitlab-ci.yml
+++ b/.gitlab/ci/review.gitlab-ci.yml
@@ -75,6 +75,7 @@ review-build-cng:
name: review/${CI_COMMIT_REF_NAME}
url: https://gitlab-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}
on_stop: review-stop
+ auto_stop_in: 48 hours
review-deploy:
extends:
diff --git a/CHANGELOG-EE.md b/CHANGELOG-EE.md
index fe723f5d1ef..f2068d36ffc 100644
--- a/CHANGELOG-EE.md
+++ b/CHANGELOG-EE.md
@@ -1,5 +1,16 @@
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)
diff --git a/app/assets/javascripts/error_tracking/components/error_details.vue b/app/assets/javascripts/error_tracking/components/error_details.vue
index 093d993c3ad..43fa97e4095 100644
--- a/app/assets/javascripts/error_tracking/components/error_details.vue
+++ b/app/assets/javascripts/error_tracking/components/error_details.vue
@@ -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">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
index 57be97855e3..b1fb377e47a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
@@ -1,5 +1,6 @@
<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,
);
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 82bef91230e..8b0dd73c565 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -1090,6 +1090,20 @@ button.mini-pipeline-graph-dropdown-toggle {
}
}
+.codequality-report {
+ .media {
+ padding: $gl-padding;
+ }
+
+ .media-body {
+ flex-direction: row;
+ }
+
+ .report-block-container {
+ height: auto !important;
+ }
+}
+
.progress-bar.bg-primary {
background-color: $blue-500 !important;
}
diff --git a/app/controllers/concerns/uploads_actions.rb b/app/controllers/concerns/uploads_actions.rb
index 655575e0944..549a443b1a8 100644
--- a/app/controllers/concerns/uploads_actions.rb
+++ b/app/controllers/concerns/uploads_actions.rb
@@ -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
diff --git a/app/controllers/groups/group_links_controller.rb b/app/controllers/groups/group_links_controller.rb
index d3360acd245..23daa29ac43 100644
--- a/app/controllers/groups/group_links_controller.rb
+++ b/app/controllers/groups/group_links_controller.rb
@@ -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
diff --git a/app/graphql/types/diff_refs_type.rb b/app/graphql/types/diff_refs_type.rb
index 03d080d784b..4049a204f66 100644
--- a/app/graphql/types/diff_refs_type.rb
+++ b/app/graphql/types/diff_refs_type.rb
@@ -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'
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index f9d7e8a2b7a..481e1807a78 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -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')
diff --git a/app/models/badge.rb b/app/models/badge.rb
index eb351425e66..3400d6d407d 100644
--- a/app/models/badge.rb
+++ b/app/models/badge.rb
@@ -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
diff --git a/app/models/group.rb b/app/models/group.rb
index d6a4af5af15..e9b3e3c3369 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -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))
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index bdff9e28df1..bc3be67bd32 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -66,6 +66,7 @@ class GroupMember < Member
def after_accept_invite
notification_service.accept_group_invite(self)
+ update_two_factor_requirement
super
end
diff --git a/app/models/user_detail.rb b/app/models/user_detail.rb
index 1621f336111..5dc74421705 100644
--- a/app/models/user_detail.rb
+++ b/app/models/user_detail.rb
@@ -3,5 +3,5 @@
class UserDetail < ApplicationRecord
belongs_to :user
- validates :job_title, presence: true, length: { maximum: 200 }
+ validates :job_title, length: { maximum: 200 }
end
diff --git a/app/presenters/ci/pipeline_presenter.rb b/app/presenters/ci/pipeline_presenter.rb
index 37abefb5664..ce9a3346b4b 100644
--- a/app/presenters/ci/pipeline_presenter.rb
+++ b/app/presenters/ci/pipeline_presenter.rb
@@ -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
diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb
index 09a84950755..629c1cbdc5c 100644
--- a/app/services/auth/container_registry_authentication_service.rb
+++ b/app/services/auth/container_registry_authentication_service.rb
@@ -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
diff --git a/app/services/groups/group_links/destroy_service.rb b/app/services/groups/group_links/destroy_service.rb
index 29aa8de4e68..6835b6c4637 100644
--- a/app/services/groups/group_links/destroy_service.rb
+++ b/app/services/groups/group_links/destroy_service.rb
@@ -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
diff --git a/app/services/groups/group_links/update_service.rb b/app/services/groups/group_links/update_service.rb
new file mode 100644
index 00000000000..71b52cb616c
--- /dev/null
+++ b/app/services/groups/group_links/update_service.rb
@@ -0,0 +1,29 @@
+# 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
diff --git a/app/services/projects/lfs_pointers/lfs_download_service.rb b/app/services/projects/lfs_pointers/lfs_download_service.rb
index bd70012c76c..52c73bcff03 100644
--- a/app/services/projects/lfs_pointers/lfs_download_service.rb
+++ b/app/services/projects/lfs_pointers/lfs_download_service.rb
@@ -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|
diff --git a/app/services/projects/lfs_pointers/lfs_object_download_list_service.rb b/app/services/projects/lfs_pointers/lfs_object_download_list_service.rb
index d6e6480bdad..75106297043 100644
--- a/app/services/projects/lfs_pointers/lfs_object_download_list_service.rb
+++ b/app/services/projects/lfs_pointers/lfs_object_download_list_service.rb
@@ -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
diff --git a/app/services/web_hook_service.rb b/app/services/web_hook_service.rb
index 514ba998d2c..178a321e20c 100644
--- a/app/services/web_hook_service.rb
+++ b/app/services/web_hook_service.rb
@@ -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
diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb
index 0fc71d2e3f3..505b51c2006 100644
--- a/app/uploaders/file_uploader.rb
+++ b/app/uploaders/file_uploader.rb
@@ -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
diff --git a/app/validators/addressable_url_validator.rb b/app/validators/addressable_url_validator.rb
index 300bd01ed22..99f503c3f06 100644
--- a/app/validators/addressable_url_validator.rb
+++ b/app/validators/addressable_url_validator.rb
@@ -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
diff --git a/app/views/admin/application_settings/_grafana.html.haml b/app/views/admin/application_settings/_grafana.html.haml
index b6e02bde895..700be7db54f 100644
--- a/app/views/admin/application_settings/_grafana.html.haml
+++ b/app/views/admin/application_settings/_grafana.html.haml
@@ -1,4 +1,4 @@
-= 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
diff --git a/app/views/clusters/clusters/_form.html.haml b/app/views/clusters/clusters/_form.html.haml
index a85b005b2b4..40fc7d110fc 100644
--- a/app/views/clusters/clusters/_form.html.haml
+++ b/app/views/clusters/clusters/_form.html.haml
@@ -3,7 +3,7 @@
.form-group
%h5= s_('ClusterIntegration|Integration status')
%label.append-bottom-0.js-cluster-enable-toggle-area
- = render "shared/buttons/project_feature_toggle", is_checked: @cluster.enabled?, label: s_("ClusterIntegration|Toggle Kubernetes cluster"), disabled: !can?(current_user, :update_cluster, @cluster) do
+ = render "shared/buttons/project_feature_toggle", is_checked: @cluster.enabled?, label: s_("ClusterIntegration|Toggle Kubernetes cluster"), disabled: !can?(current_user, :update_cluster, @cluster), data: { qa_selector: 'integration_status_toggle' } do
= field.hidden_field :enabled, { class: 'js-project-feature-toggle-input'}
.form-text.text-muted= s_('ClusterIntegration|Enable or disable GitLab\'s connection to your Kubernetes cluster.')
@@ -22,7 +22,7 @@
.form-group
%h5= s_('ClusterIntegration|Base domain')
- = field.text_field :base_domain, class: 'col-md-6 form-control js-select-on-focus qa-base-domain'
+ = field.text_field :base_domain, class: 'col-md-6 form-control js-select-on-focus', data: { qa_selector: 'base_domain_field' }
.form-text.text-muted
- auto_devops_url = help_page_path('topics/autodevops/index')
- auto_devops_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: auto_devops_url }
@@ -37,4 +37,4 @@
- if can?(current_user, :update_cluster, @cluster)
.form-group
- = field.submit _('Save changes'), class: 'btn btn-success qa-save-domain'
+ = field.submit _('Save changes'), class: 'btn btn-success', data: { qa_selector: 'save_changes_button' }
diff --git a/app/views/clusters/clusters/user/_form.html.haml b/app/views/clusters/clusters/user/_form.html.haml
index 39b6d74d9f9..ce226d29113 100644
--- a/app/views/clusters/clusters/user/_form.html.haml
+++ b/app/views/clusters/clusters/user/_form.html.haml
@@ -54,4 +54,4 @@
= render('clusters/clusters/namespace', platform_field: platform_kubernetes_field)
.form-group
- = field.submit s_('ClusterIntegration|Add Kubernetes cluster'), class: 'btn btn-success'
+ = field.submit s_('ClusterIntegration|Add Kubernetes cluster'), class: 'btn btn-success', data: { qa_selector: 'add_kubernetes_cluster_button' }
diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml
index 9f70124ba0d..78cd3f62dec 100644
--- a/app/views/layouts/nav/sidebar/_admin.html.haml
+++ b/app/views/layouts/nav/sidebar/_admin.html.haml
@@ -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'
diff --git a/app/views/shared/issuable/form/_branch_chooser.html.haml b/app/views/shared/issuable/form/_branch_chooser.html.haml
index 29ac17c43b9..8d9e5ddf065 100644
--- a/app/views/shared/issuable/form/_branch_chooser.html.haml
+++ b/app/views/shared/issuable/form/_branch_chooser.html.haml
@@ -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;
diff --git a/changelogs/unreleased/208675-add-package_name-as-option-to-packages-api.yml b/changelogs/unreleased/208675-add-package_name-as-option-to-packages-api.yml
new file mode 100644
index 00000000000..3f7a0e3f62d
--- /dev/null
+++ b/changelogs/unreleased/208675-add-package_name-as-option-to-packages-api.yml
@@ -0,0 +1,5 @@
+---
+title: Added package_name as filter parameter to packages API
+merge_request: 26291
+author:
+type: added
diff --git a/changelogs/unreleased/georgekoltsov-27883-fix-import-pipeline-order.yml b/changelogs/unreleased/georgekoltsov-27883-fix-import-pipeline-order.yml
new file mode 100644
index 00000000000..1ccabf0af1a
--- /dev/null
+++ b/changelogs/unreleased/georgekoltsov-27883-fix-import-pipeline-order.yml
@@ -0,0 +1,5 @@
+---
+title: Fix reversed pipeline order on Project Import
+merge_request: 26390
+author:
+type: fixed
diff --git a/config/initializers/8_devise.rb b/config/initializers/8_devise.rb
index e1c37caaafd..6ed56598e15 100644
--- a/config/initializers/8_devise.rb
+++ b/config/initializers/8_devise.rb
@@ -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 ]
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index 22536b60ca1..88e198116a0 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -36,15 +36,6 @@ use_sidekiq_legacy_memory_killer = !use_sidekiq_daemon_memory_killer
use_request_store = ENV.fetch('SIDEKIQ_REQUEST_STORE', 1).to_i.nonzero?
Sidekiq.configure_server do |config|
- config.redis = queues_config_hash
-
- config.server_middleware(&Gitlab::SidekiqMiddleware.server_configurator({
- metrics: Settings.monitoring.sidekiq_exporter,
- arguments_logger: ENV['SIDEKIQ_LOG_ARGUMENTS'] && !enable_json_logs,
- memory_killer: enable_sidekiq_memory_killer && use_sidekiq_legacy_memory_killer,
- request_store: use_request_store
- }))
-
if enable_json_logs
Sidekiq.logger.formatter = Gitlab::SidekiqLogging::JSONFormatter.new
config.options[:job_logger] = Gitlab::SidekiqLogging::StructuredLogger
@@ -54,6 +45,15 @@ Sidekiq.configure_server do |config|
config.error_handlers << Gitlab::SidekiqLogging::ExceptionHandler.new
end
+ config.redis = queues_config_hash
+
+ config.server_middleware(&Gitlab::SidekiqMiddleware.server_configurator({
+ metrics: Settings.monitoring.sidekiq_exporter,
+ arguments_logger: ENV['SIDEKIQ_LOG_ARGUMENTS'] && !enable_json_logs,
+ memory_killer: enable_sidekiq_memory_killer && use_sidekiq_legacy_memory_killer,
+ request_store: use_request_store
+ }))
+
config.client_middleware(&Gitlab::SidekiqMiddleware.client_configurator)
config.on :startup do
diff --git a/db/migrate/20200214085940_clean_grafana_url.rb b/db/migrate/20200214085940_clean_grafana_url.rb
new file mode 100644
index 00000000000..7dff6532516
--- /dev/null
+++ b/db/migrate/20200214085940_clean_grafana_url.rb
@@ -0,0 +1,22 @@
+# 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
diff --git a/db/post_migrate/20200204113224_schedule_recalculate_project_authorizations_second_run.rb b/db/post_migrate/20200204113224_schedule_recalculate_project_authorizations_second_run.rb
new file mode 100644
index 00000000000..8f4a347b5e2
--- /dev/null
+++ b/db/post_migrate/20200204113224_schedule_recalculate_project_authorizations_second_run.rb
@@ -0,0 +1,32 @@
+# 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
diff --git a/doc/administration/high_availability/README.md b/doc/administration/high_availability/README.md
index 4f9e3b1a180..c8783b8ff5f 100644
--- a/doc/administration/high_availability/README.md
+++ b/doc/administration/high_availability/README.md
@@ -76,7 +76,7 @@ On different cloud vendors a best effort like for like can be used.
- **Known Issues:** For the latest list of known performance issues head
[here](https://gitlab.com/gitlab-org/gitlab/issues?label_name%5B%5D=Quality%3Aperformance-issues).
-| Service | Nodes | Configuration | GCP type |
+| Service | Nodes | Configuration[^8] | GCP type |
| ----------------------------|-------|-----------------------|---------------|
| GitLab Rails[^1] | 3 | 8 vCPU, 7.2GB Memory | n1-highcpu-8 |
| PostgreSQL | 3 | 2 vCPU, 7.5GB Memory | n1-standard-2 |
@@ -98,7 +98,7 @@ On different cloud vendors a best effort like for like can be used.
- **Known Issues:** For the latest list of known performance issues head
[here](https://gitlab.com/gitlab-org/gitlab/issues?label_name%5B%5D=Quality%3Aperformance-issues).
-| Service | Nodes | Configuration | GCP type |
+| Service | Nodes | Configuration[^8] | GCP type |
| ----------------------------|-------|-----------------------|---------------|
| GitLab Rails[^1] | 3 | 16 vCPU, 14.4GB Memory | n1-highcpu-16 |
| PostgreSQL | 3 | 2 vCPU, 7.5GB Memory | n1-standard-2 |
@@ -120,7 +120,7 @@ On different cloud vendors a best effort like for like can be used.
- **Known Issues:** For the latest list of known performance issues head
[here](https://gitlab.com/gitlab-org/gitlab/issues?label_name%5B%5D=Quality%3Aperformance-issues).
-| Service | Nodes | Configuration | GCP type |
+| Service | Nodes | Configuration[^8] | GCP type |
| ----------------------------|-------|-----------------------|---------------|
| GitLab Rails[^1] | 3 | 32 vCPU, 28.8GB Memory | n1-highcpu-32 |
| PostgreSQL | 3 | 4 vCPU, 15GB Memory | n1-standard-4 |
@@ -145,9 +145,9 @@ On different cloud vendors a best effort like for like can be used.
- **Known Issues:** For the latest list of known performance issues head
[here](https://gitlab.com/gitlab-org/gitlab/issues?label_name%5B%5D=Quality%3Aperformance-issues).
-| Service | Nodes | Configuration | GCP type |
+| Service | Nodes | Configuration[^8] | GCP type |
| ----------------------------|-------|-----------------------|---------------|
-| GitLab Rails[^1] | 7 | 32 vCPU, 28.8GB Memory | n1-highcpu-32 |
+| GitLab Rails[^1] | 5 | 32 vCPU, 28.8GB Memory | n1-highcpu-32 |
| PostgreSQL | 3 | 8 vCPU, 30GB Memory | n1-standard-8 |
| PgBouncer | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
| Gitaly[^2] [^5] [^7] | X | 32 vCPU, 120GB Memory | n1-standard-32 |
@@ -170,9 +170,9 @@ On different cloud vendors a best effort like for like can be used.
- **Known Issues:** For the latest list of known performance issues head
[here](https://gitlab.com/gitlab-org/gitlab/issues?label_name%5B%5D=Quality%3Aperformance-issues).
-| Service | Nodes | Configuration | GCP type |
+| Service | Nodes | Configuration[^8] | GCP type |
| ----------------------------|-------|-----------------------|---------------|
-| GitLab Rails[^1] | 15 | 32 vCPU, 28.8GB Memory | n1-highcpu-32 |
+| GitLab Rails[^1] | 12 | 32 vCPU, 28.8GB Memory | n1-highcpu-32 |
| PostgreSQL | 3 | 16 vCPU, 60GB Memory | n1-standard-16 |
| PgBouncer | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
| Gitaly[^2] [^5] [^7] | X | 64 vCPU, 240GB Memory | n1-standard-64 |
@@ -230,3 +230,9 @@ On different cloud vendors a best effort like for like can be used.
as with time they may be adjusted higher or lower depending on the scale of your
environment's workload. If you're running the environment on a Cloud provider
you may need to refer to their documentation on how configure IOPS correctly.
+
+[^8]: The architectures were built and tested with the [Intel Xeon E5 v3 (Haswell)](https://cloud.google.com/compute/docs/cpu-platforms)
+ CPU platform on GCP. On different hardware you may find that adjustments, either lower
+ or higher, are required for your CPU or Node counts accordingly. For more info a
+ [Sysbench](https://github.com/akopytov/sysbench) benchmark of the CPU can be found
+ [here](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Reference-Architectures/GCP-CPU-Benchmarks).
diff --git a/doc/administration/monitoring/performance/grafana_configuration.md b/doc/administration/monitoring/performance/grafana_configuration.md
index 12b69d10179..61b570aa42e 100644
--- a/doc/administration/monitoring/performance/grafana_configuration.md
+++ b/doc/administration/monitoring/performance/grafana_configuration.md
@@ -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**.
diff --git a/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md b/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md
index d1aefb3219c..16eb3269ef0 100644
--- a/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md
+++ b/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md
@@ -262,14 +262,6 @@ p.each do |project|
end
```
-### Identify un-indexed projects
-
-```ruby
-Project.find_each do |project|
- puts "id #{project.id}: #{project.namespace.name.to_s}/#{project.name.to_s}" if project.index_status.nil?
-end
-```
-
## Wikis
### Recreate
@@ -761,12 +753,6 @@ Ci::Pipeline.where(project_id: p.id).where(status: 'pending').each {|p| p.cancel
Ci::Pipeline.where(project_id: p.id).where(status: 'pending').count
```
-### Manually modify runner minutes
-
-```ruby
-Namespace.find_by_full_path("user/proj").namespace_statistics.update(shared_runners_seconds: 27360)
-```
-
### Remove artifacts more than a week old
The Latest version of these steps can be found in the [job artifacts documentation](../job_artifacts.md)
@@ -806,21 +792,6 @@ build.dependencies.each do |d| { puts "status: #{d.status}, finished at: #{d.fin
completed: #{d.complete?}, artifacts_expired: #{d.artifacts_expired?}, erased: #{d.erased?}" }
```
-### Disable strict artifact checking (Introduced in GitLab 10.3.0)
-
-See [job artifacts documentation](../job_artifacts.md#validation-for-dependencies).
-
-```ruby
-Feature.enable('ci_disable_validates_dependencies')
-```
-
-### Remove CI traces older than 6 months
-
-```ruby
-current_user = User.find_by_email('cindy@gitlap.com')
-Ci::Build.where("finished_at < ?", 6.months.ago.to_date).each {|b| puts b.id; b.erase(erased_by: current_user) if b.erasable?};nil
-```
-
### Try CI service
```ruby
@@ -965,12 +936,6 @@ end
## Sidekiq
-### Size of a queue
-
-```ruby
-Sidekiq::Queue.new('background_migration').size
-```
-
### Kill a worker's Sidekiq jobs
```ruby
@@ -1017,12 +982,6 @@ See <https://github.com/mperham/sidekiq/wiki/Signals#ttin>.
/opt/gitlab/embedded/bin/redis-cli -s /var/opt/gitlab/redis/redis.socket
```
-### Connect to Redis (HA)
-
-```shell
-/opt/gitlab/embedded/bin/redis-cli -h <host ip> -a <password>
-```
-
## LFS
### Get info about LFS objects and associated project
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index 7dda073d8ae..336c21ef6a1 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -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
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index 83b098dc73d..4d52f45d3e4 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -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
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 79be0c0d626..923cc7dff3d 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -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 |
diff --git a/doc/api/packages.md b/doc/api/packages.md
index 097f9dff84e..b1b6969769c 100644
--- a/doc/api/packages.md
+++ b/doc/api/packages.md
@@ -21,6 +21,7 @@ GET /projects/:id/packages
| `order_by`| string | no | The field to use as order. One of `created_at` (default), `name`, `version`, or `type`. |
| `sort` | string | no | The direction of the order, either `asc` (default) for ascending order or `desc` for descending order. |
| `package_type` | string | no | Filter the returned packages by type. One of `conan`, `maven`, `npm` or `nuget`. (_Introduced in GitLab 12.9_)
+| `package_name` | string | no | Filter the project packages with a fuzzy search by name. (_Introduced in GitLab 12.9_)
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/:id/packages
diff --git a/doc/development/testing_guide/review_apps.md b/doc/development/testing_guide/review_apps.md
index 3307f29a98e..b8df3286da2 100644
--- a/doc/development/testing_guide/review_apps.md
+++ b/doc/development/testing_guide/review_apps.md
@@ -79,27 +79,38 @@ subgraph "CNG-mirror pipeline"
**Additional notes:**
- If the `review-deploy` job keep failing (note that we already retry it twice),
- please post a message in the `#quality` channel and/or create a ~Quality ~bug
+ please post a message in the `#g_qe_engineering_productivity` channel and/or create a `~"Engineering Productivity"` `~"ep::review apps"` `~bug`
issue with a link to your merge request. Note that the deployment failure can
reveal an actual problem introduced in your merge request (i.e. this isn't
necessarily a transient failure)!
-- If the `review-qa-smoke` job keep failing (note that we already retry it twice),
+- If the `review-qa-smoke` job keeps failing (note that we already retry it twice),
please check the job's logs: you could discover an actual problem introduced in
your merge request. You can also download the artifacts to see screenshots of
the page at the time the failures occurred. If you don't find the cause of the
failure or if it seems unrelated to your change, please post a message in the
`#quality` channel and/or create a ~Quality ~bug issue with a link to your
merge request.
-- The manual [`review-stop`][gitlab-ci-yml] in the `test` stage can be used to
+- The manual `review-stop` can be used to
stop a Review App manually, and is also started by GitLab once a merge
request's branch is deleted after being merged.
-- Review Apps are cleaned up regularly via a pipeline schedule that runs
- the [`schedule:review-cleanup`][gitlab-ci-yml] job.
- The Kubernetes cluster is connected to the `gitlab-{ce,ee}` projects using
[GitLab's Kubernetes integration][gitlab-k8s-integration]. This basically
allows to have a link to the Review App directly from the merge request
widget.
+### Auto-stopping of Review Apps
+
+Review Apps are automatically stopped 2 days after the last deployment thanks to
+the [Environment auto-stop](../../ci/environments.html#environments-auto-stop) feature.
+
+If you need your Review App to stay up for a longer time, you can
+[pin its environment](../../ci/environments.html#auto-stop-example).
+
+The `review-cleanup` job that automatically runs in scheduled
+pipelines (and is manual in merge request) stops stale Review Apps after 5 days,
+deletes their environment after 6 days, and cleans up any dangling Helm releases
+and Kubernetes resources after 7 days.
+
## QA runs
On every [pipeline][gitlab-pipeline] in the `qa` stage (which comes after the
@@ -206,7 +217,7 @@ aids in identifying load spikes on the cluster, and if nodes are problematic or
**Potential cause:**
-That could be a sign that the [`schedule:review-cleanup`][gitlab-ci-yml] job is
+That could be a sign that the `review-cleanup` job is
failing to cleanup stale Review Apps and Kubernetes resources.
**Where to look for further debugging:**
@@ -270,7 +281,7 @@ kubectl get cm --sort-by='{.metadata.creationTimestamp}' | grep 'review-' | grep
### Using K9s
-[K9s] is a powerful command line dashboard which allows you to filter by labels. This can help identify trends with apps exceeding the [review-app resource requests](https://gitlab.com/gitlab-org/gitlab/blob/master/scripts/review_apps/base-config.yaml). Kubernetes will schedule pods to nodes based on resource requests and allow for CPU usage up to the limits.
+[K9s] is a powerful command line dashboard which allows you to filter by labels. This can help identify trends with apps exceeding the [review-app resource requests](https://gitlab.com/gitlab-org/gitlab/-/blob/master/scripts/review_apps/base-config.yaml). Kubernetes will schedule pods to nodes based on resource requests and allow for CPU usage up to the limits.
- In K9s you can sort or add filters by typing the `/` character
- `-lrelease=<review-app-slug>` - filters down to all pods for a release. This aids in determining what is having issues in a single deployment
@@ -387,13 +398,11 @@ find a way to limit it to only us.**
[helm-chart]: https://gitlab.com/gitlab-org/charts/gitlab/
[review-apps-ce]: https://console.cloud.google.com/kubernetes/clusters/details/us-central1-a/review-apps-ce?project=gitlab-review-apps
[review-apps-ee]: https://console.cloud.google.com/kubernetes/clusters/details/us-central1-b/review-apps-ee?project=gitlab-review-apps
-[review-apps.sh]: https://gitlab.com/gitlab-org/gitlab/blob/master/scripts/review_apps/review-apps.sh
-[automated_cleanup.rb]: https://gitlab.com/gitlab-org/gitlab/blob/master/scripts/review_apps/automated_cleanup.rb
-[Auto-DevOps.gitlab-ci.yml]: https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
-[gitlab-ci-yml]: https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab-ci.yml
+[review-apps.sh]: https://gitlab.com/gitlab-org/gitlab/-/blob/master/scripts/review_apps/review-apps.sh
+[automated_cleanup.rb]: https://gitlab.com/gitlab-org/gitlab/-/blob/master/scripts/review_apps/automated_cleanup.rb
+[Auto-DevOps.gitlab-ci.yml]: https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
[gitlab-k8s-integration]: ../../user/project/clusters/index.md
[K9s]: https://github.com/derailed/k9s
-[password-bug]: https://gitlab.com/gitlab-org/gitlab-foss/issues/53621
---
diff --git a/doc/user/project/merge_requests/code_quality.md b/doc/user/project/merge_requests/code_quality.md
index 91cdddc9300..b2b28c54350 100644
--- a/doc/user/project/merge_requests/code_quality.md
+++ b/doc/user/project/merge_requests/code_quality.md
@@ -260,11 +260,16 @@ GitLab.
## Code Quality reports
-Once the Code Quality job has completed, GitLab:
+Once the Code Quality job has completed:
-- Checks the generated report.
-- Compares the metrics between the source and target branches.
-- Shows the information right on the merge request.
+- The full list of code quality violations generated by a pipeline is available in the
+ Code Quality tab of the Pipeline Details page.
+- Potential changes to code quality are shown directly in the merge request.
+ The Code Quality widget in the merge request compares the reports from the base and head of the branch,
+ then lists any violations that will be resolved or created when the branch is merged.
+- The full JSON report is available as a
+ [downloadable artifact](../../project/pipelines/job_artifacts.html#downloading-artifacts)
+ for the `code_quality` job.
If multiple jobs in a pipeline generate a code quality artifact, only the artifact from
the last created job (the job with the largest job ID) is used. To avoid confusion,
@@ -276,6 +281,10 @@ Code Quality job in your `.gitlab-ci.yml` for the very first time.
Consecutive merge requests will have something to compare to and the Code Quality
report will be shown properly.
+These reports will only be available as long as the Code Quality artifact(s) required to generate
+them are also available. See
+[`artifacts:expire_in`](../../../ci/yaml/README.md#artifactsexpire_in) for more details.
+
<!-- ## Troubleshooting
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb
index ab83d84284f..76af29b2977 100644
--- a/lib/api/triggers.rb
+++ b/lib/api/triggers.rb
@@ -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
diff --git a/lib/gitlab/asset_proxy.rb b/lib/gitlab/asset_proxy.rb
new file mode 100644
index 00000000000..fd7c58ba68f
--- /dev/null
+++ b/lib/gitlab/asset_proxy.rb
@@ -0,0 +1,33 @@
+# 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
diff --git a/lib/gitlab/background_migration/recalculate_project_authorizations_with_min_max_user_id.rb b/lib/gitlab/background_migration/recalculate_project_authorizations_with_min_max_user_id.rb
new file mode 100644
index 00000000000..b66fdfd5c65
--- /dev/null
+++ b/lib/gitlab/background_migration/recalculate_project_authorizations_with_min_max_user_id.rb
@@ -0,0 +1,38 @@
+# 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
diff --git a/lib/gitlab/dependency_linker/base_linker.rb b/lib/gitlab/dependency_linker/base_linker.rb
index dd7ab92c6ae..a4e265eba88 100644
--- a/lib/gitlab/dependency_linker/base_linker.rb
+++ b/lib/gitlab/dependency_linker/base_linker.rb
@@ -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.
diff --git a/lib/gitlab/import_export/relation_tree_restorer.rb b/lib/gitlab/import_export/relation_tree_restorer.rb
index 1797bbad51a..e0d6ade51c2 100644
--- a/lib/gitlab/import_export/relation_tree_restorer.rb
+++ b/lib/gitlab/import_export/relation_tree_restorer.rb
@@ -26,6 +26,7 @@ module Gitlab
ActiveRecord::Base.uncached do
ActiveRecord::Base.no_touching do
update_params!
+ update_relation_hashes!
create_relations!
end
end
@@ -217,6 +218,10 @@ module Gitlab
excluded_keys: excluded_keys_for_relation(relation_key)
}
end
+
+ def update_relation_hashes!
+ @tree_hash['ci_pipelines']&.sort_by! { |hash| hash['id'] }
+ end
end
end
end
diff --git a/lib/gitlab/project_authorizations.rb b/lib/gitlab/project_authorizations.rb
index d65e8759ec9..ff90a009b2e 100644
--- a/lib/gitlab/project_authorizations.rb
+++ b/lib/gitlab/project_authorizations.rb
@@ -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)
diff --git a/lib/gitlab/sidekiq_logging/json_formatter.rb b/lib/gitlab/sidekiq_logging/json_formatter.rb
index e0b0d684bea..c20e929ae36 100644
--- a/lib/gitlab/sidekiq_logging/json_formatter.rb
+++ b/lib/gitlab/sidekiq_logging/json_formatter.rb
@@ -1,5 +1,8 @@
# frozen_string_literal: true
+# This is needed for sidekiq-cluster
+require 'json'
+
module Gitlab
module SidekiqLogging
class JSONFormatter
diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb
index ae4c77255c5..fda2985307e 100644
--- a/lib/gitlab/user_access.rb
+++ b/lib/gitlab/user_access.rb
@@ -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)
diff --git a/lib/gitlab/utils.rb b/lib/gitlab/utils.rb
index 5727e73e725..ad6b213bb50 100644
--- a/lib/gitlab/utils.rb
+++ b/lib/gitlab/utils.rb
@@ -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
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 0a63aca949d..c6738dafa23 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -68,6 +68,11 @@ msgstr ""
msgid "\"%{path}\" did not exist on \"%{ref}\""
msgstr ""
+msgid "%d code quality issue"
+msgid_plural "%d code quality issues"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d comment"
msgid_plural "%d comments"
msgstr[0] ""
@@ -4853,6 +4858,9 @@ msgstr ""
msgid "Code Owners to the merge request changes."
msgstr ""
+msgid "Code Quality"
+msgstr ""
+
msgid "Code Review"
msgstr ""
@@ -8378,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 ""
@@ -8825,7 +8836,7 @@ msgstr ""
msgid "From %{providerTitle}"
msgstr ""
-msgid "From %{source_title} into"
+msgid "From <code>%{source_title}</code> into"
msgstr ""
msgid "From Bitbucket"
@@ -23188,6 +23199,9 @@ msgstr ""
msgid "ciReport|Fixed:"
msgstr ""
+msgid "ciReport|Found %{issuesWithCount}"
+msgstr ""
+
msgid "ciReport|Investigate this vulnerability by creating an issue"
msgstr ""
@@ -23206,6 +23220,9 @@ msgstr ""
msgid "ciReport|No changes to performance metrics"
msgstr ""
+msgid "ciReport|No code quality issues found"
+msgstr ""
+
msgid "ciReport|Performance metrics"
msgstr ""
@@ -23236,6 +23253,9 @@ msgstr ""
msgid "ciReport|There was an error dismissing the vulnerability. Please try again."
msgstr ""
+msgid "ciReport|There was an error fetching the codequality report."
+msgstr ""
+
msgid "ciReport|There was an error reverting the dismissal. Please try again."
msgstr ""
diff --git a/qa/qa.rb b/qa/qa.rb
index ac5569bfad8..57b622c7d93 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -429,6 +429,7 @@ module QA
autoload :Gcloud, 'qa/service/cluster_provider/gcloud'
autoload :Minikube, 'qa/service/cluster_provider/minikube'
autoload :K3d, 'qa/service/cluster_provider/k3d'
+ autoload :K3s, 'qa/service/cluster_provider/k3s'
end
module DockerRun
@@ -440,6 +441,7 @@ module QA
autoload :GitlabRunner, 'qa/service/docker_run/gitlab_runner'
autoload :MailHog, 'qa/service/docker_run/mail_hog'
autoload :SamlIdp, 'qa/service/docker_run/saml_idp'
+ autoload :K3s, 'qa/service/docker_run/k3s'
end
end
diff --git a/qa/qa/page/project/operations/kubernetes/add_existing.rb b/qa/qa/page/project/operations/kubernetes/add_existing.rb
index 9f47841366e..c143b55d057 100644
--- a/qa/qa/page/project/operations/kubernetes/add_existing.rb
+++ b/qa/qa/page/project/operations/kubernetes/add_existing.rb
@@ -11,7 +11,7 @@ module QA
element :api_url, 'url_field :api_url' # rubocop:disable QA/ElementWithPattern
element :ca_certificate, 'text_area :ca_cert' # rubocop:disable QA/ElementWithPattern
element :token, 'text_field :token' # rubocop:disable QA/ElementWithPattern
- element :add_cluster_button, "submit s_('ClusterIntegration|Add Kubernetes cluster')" # rubocop:disable QA/ElementWithPattern
+ element :add_kubernetes_cluster_button
element :rbac_checkbox
end
@@ -32,7 +32,7 @@ module QA
end
def add_cluster!
- click_on 'Add Kubernetes cluster'
+ click_element :add_kubernetes_cluster_button, Page::Project::Operations::Kubernetes::Show
end
def uncheck_rbac!
diff --git a/qa/qa/page/project/operations/kubernetes/show.rb b/qa/qa/page/project/operations/kubernetes/show.rb
index 3d3eebdbec9..b639f867593 100644
--- a/qa/qa/page/project/operations/kubernetes/show.rb
+++ b/qa/qa/page/project/operations/kubernetes/show.rb
@@ -11,14 +11,20 @@ module QA
end
view 'app/views/clusters/clusters/_form.html.haml' do
- element :base_domain
- element :save_domain
+ element :integration_status_toggle, required: true
+ element :base_domain_field, required: true
+ element :save_changes_button, required: true
+ end
+
+ view 'app/assets/javascripts/clusters/components/application_row.vue' do
+ element :install_button
+ element :uninstall_button
end
def install!(application_name)
within_element(application_name) do
has_element?(:install_button, application: application_name, wait: 30)
- click_on 'Install' # TODO replace with click_element
+ click_element :install_button
end
end
@@ -41,11 +47,11 @@ module QA
end
def set_domain(domain)
- fill_element :base_domain, domain
+ fill_element :base_domain_field, domain
end
def save_domain
- click_element :save_domain
+ click_element :save_changes_button, Page::Project::Operations::Kubernetes::Show
end
end
end
diff --git a/qa/qa/service/cluster_provider/k3s.rb b/qa/qa/service/cluster_provider/k3s.rb
new file mode 100644
index 00000000000..165de795683
--- /dev/null
+++ b/qa/qa/service/cluster_provider/k3s.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+module QA
+ module Service
+ module ClusterProvider
+ class K3s < Base
+ def validate_dependencies
+ Runtime::ApplicationSettings.set_application_settings(allow_local_requests_from_web_hooks_and_services: true)
+ end
+
+ def setup
+ @k3s = Service::DockerRun::K3s.new.tap do |k3s|
+ k3s.register!
+
+ shell "kubectl config set-cluster k3s --server https://#{k3s.host_name}:6443 --insecure-skip-tls-verify"
+ shell 'kubectl config set-credentials default --username=node --password=some-secret'
+ shell 'kubectl config set-context k3s --cluster=k3s --user=default'
+ shell 'kubectl config use-context k3s'
+
+ wait_for_server(k3s.host_name) do
+ shell 'kubectl version'
+
+ wait_for_namespaces do
+ # install local storage
+ shell 'kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml'
+
+ # patch local storage
+ shell %(kubectl patch storageclass local-path -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}')
+ end
+ end
+ end
+ end
+
+ def teardown
+ Runtime::ApplicationSettings.set_application_settings(allow_local_requests_from_web_hooks_and_services: false)
+
+ @k3s&.remove!
+ end
+
+ def set_credentials(admin_user)
+ end
+
+ # Fetch "real" certificate
+ # See https://github.com/rancher/k3s/issues/27
+ def filter_credentials(credentials)
+ kubeconfig = YAML.safe_load(@k3s.kubeconfig)
+ ca_certificate = kubeconfig.dig('clusters', 0, 'cluster', 'certificate-authority-data')
+
+ credentials.merge('data' => credentials['data'].merge('ca.crt' => ca_certificate))
+ end
+
+ private
+
+ def wait_for_server(host_name)
+ print "Waiting for K3s server at `https://#{host_name}:6443` to become available "
+
+ 60.times do
+ if service_available?('kubectl version')
+ return yield if block_given?
+
+ return true
+ end
+
+ sleep 1
+ print '.'
+ end
+
+ raise 'K3s server never came up'
+ end
+
+ def wait_for_namespaces
+ print 'Waiting for k8s namespaces to populate'
+
+ 60.times do
+ if service_available?('kubectl get pods --all-namespaces | grep --silent "Running"')
+ return yield if block_given?
+
+ return true
+ end
+
+ sleep 1
+ print '.'
+ end
+
+ raise 'K8s namespaces didnt populate correctly'
+ end
+
+ def service_available?(command)
+ system("#{command} > /dev/null 2>&1")
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/service/docker_run/base.rb b/qa/qa/service/docker_run/base.rb
index 3f42c09ad2c..b02bbea8ff5 100644
--- a/qa/qa/service/docker_run/base.rb
+++ b/qa/qa/service/docker_run/base.rb
@@ -37,6 +37,10 @@ module QA
def running?
`docker ps -f name=#{@name}`.include?(@name)
end
+
+ def read_file(file_path)
+ `docker exec #{@name} /bin/cat #{file_path}`
+ end
end
end
end
diff --git a/qa/qa/service/docker_run/k3s.rb b/qa/qa/service/docker_run/k3s.rb
new file mode 100644
index 00000000000..da254497ff0
--- /dev/null
+++ b/qa/qa/service/docker_run/k3s.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module QA
+ module Service
+ module DockerRun
+ class K3s < Base
+ def initialize
+ @image = 'registry.gitlab.com/gitlab-org/cluster-integration/test-utils/k3s-gitlab-ci/releases/v0.6.1'
+ @name = 'k3s'
+ super
+ end
+
+ def register!
+ pull
+ start_k3s
+ end
+
+ def host_name
+ return 'localhost' unless Runtime::Env.running_in_ci?
+
+ super
+ end
+
+ def kubeconfig
+ read_file('/etc/rancher/k3s/k3s.yaml').chomp
+ end
+
+ def start_k3s
+ command = <<~CMD.tr("\n", ' ')
+ docker run -d --rm
+ --network #{network}
+ --hostname #{host_name}
+ --name #{@name}
+ --publish 6443:6443
+ --privileged
+ #{@image} server --cluster-secret some-secret
+ CMD
+
+ command.gsub!("--network #{network} ", '') unless QA::Runtime::Env.running_in_ci?
+
+ shell command
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb
index 73b5a579e08..728f22aed89 100644
--- a/qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb
+++ b/qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb
@@ -2,10 +2,9 @@
module QA
context 'Configure' do
- # This test requires GITLAB_QA_ADMIN_ACCESS_TOKEN to be specified
- describe 'Kubernetes Cluster Integration', :orchestrated, :kubernetes, :requires_admin, :skip do
+ describe 'Kubernetes Cluster Integration', :orchestrated, :kubernetes, :requires_admin do
context 'Project Clusters' do
- let(:cluster) { Service::KubernetesCluster.new(provider_class: Service::ClusterProvider::K3d).create! }
+ let(:cluster) { Service::KubernetesCluster.new(provider_class: Service::ClusterProvider::K3s).create! }
let(:project) do
Resource::Project.fabricate_via_api! do |project|
project.name = 'project-with-k8s'
@@ -35,18 +34,6 @@ module QA
expect(index).to have_cluster(cluster)
end
end
-
- it 'installs helm and tiller on a gitlab managed app' do
- Resource::KubernetesCluster.fabricate_via_browser_ui! do |k8s_cluster|
- k8s_cluster.project = project
- k8s_cluster.cluster = cluster
- k8s_cluster.install_helm_tiller = true
- end
-
- Page::Project::Operations::Kubernetes::Show.perform do |show|
- expect(show).to have_application_installed(:helm)
- end
- end
end
end
end
diff --git a/qa/spec/service/docker_run/k3s_spec.rb b/qa/spec/service/docker_run/k3s_spec.rb
new file mode 100644
index 00000000000..0224b7d6704
--- /dev/null
+++ b/qa/spec/service/docker_run/k3s_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module QA
+ describe Service::DockerRun::K3s do
+ describe '#host_name' do
+ context 'in CI' do
+ let(:name) { 'k3s-12345' }
+ let(:network) { 'thenet' }
+
+ before do
+ allow(Runtime::Env).to receive(:running_in_ci?).and_return(true)
+ allow(subject).to receive(:network).and_return(network)
+ subject.instance_variable_set(:@name, name)
+ end
+
+ it 'returns name.network' do
+ expect(subject.host_name).to eq("#{name}.#{network}")
+ end
+ end
+
+ context 'not in CI' do
+ before do
+ allow(Runtime::Env).to receive(:running_in_ci?).and_return(false)
+ end
+
+ it 'returns localhost if not running in a CI environment' do
+ expect(subject.host_name).to eq('localhost')
+ end
+ end
+ end
+ end
+end
diff --git a/scripts/review_apps/automated_cleanup.rb b/scripts/review_apps/automated_cleanup.rb
index 8a04d8e00bc..323a1deedfc 100755
--- a/scripts/review_apps/automated_cleanup.rb
+++ b/scripts/review_apps/automated_cleanup.rb
@@ -54,7 +54,7 @@ class AutomatedCleanup
end
def perform_gitlab_environment_cleanup!(days_for_stop:, days_for_delete:)
- puts "Checking for review apps not updated in the last #{days_for_stop} days..."
+ puts "Checking for Review Apps not updated in the last #{days_for_stop} days..."
checked_environments = []
delete_threshold = threshold_time(days: days_for_delete)
@@ -84,7 +84,7 @@ class AutomatedCleanup
elsif deployed_at < stop_threshold
stop_environment(environment, deployment)
else
- print_release_state(subject: 'Review app', release_name: environment.slug, release_date: last_deploy, action: 'leaving')
+ print_release_state(subject: 'Review App', release_name: environment.slug, release_date: last_deploy, action: 'leaving')
end
checked_environments << environment.slug
@@ -94,9 +94,9 @@ class AutomatedCleanup
end
def perform_helm_releases_cleanup!(days:)
- puts "Checking for Helm releases not updated in the last #{days} days..."
+ puts "Checking for Helm releases that are FAILED or not updated in the last #{days} days..."
- threshold_day = threshold_time(days: days)
+ threshold = threshold_time(days: days)
releases_to_delete = []
@@ -104,7 +104,7 @@ class AutomatedCleanup
# Prevents deleting `dns-gitlab-review-app` releases or other unrelated releases
next unless release.name.start_with?('review-')
- if release.status == 'FAILED' || release.last_update < threshold_day
+ if release.status == 'FAILED' || release.last_update < threshold
releases_to_delete << release
else
print_release_state(subject: 'Release', release_name: release.name, release_date: release.last_update, action: 'leaving')
@@ -180,14 +180,14 @@ end
automated_cleanup = AutomatedCleanup.new
-timed('Review apps cleanup') do
- automated_cleanup.perform_gitlab_environment_cleanup!(days_for_stop: 2, days_for_delete: 3)
+timed('Review Apps cleanup') do
+ automated_cleanup.perform_gitlab_environment_cleanup!(days_for_stop: 5, days_for_delete: 6)
end
puts
timed('Helm releases cleanup') do
- automated_cleanup.perform_helm_releases_cleanup!(days: 3)
+ automated_cleanup.perform_helm_releases_cleanup!(days: 7)
end
exit(0)
diff --git a/spec/controllers/groups/group_links_controller_spec.rb b/spec/controllers/groups/group_links_controller_spec.rb
index c062de468fc..21169188386 100644
--- a/spec/controllers/groups/group_links_controller_spec.rb
+++ b/spec/controllers/groups/group_links_controller_spec.rb
@@ -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
diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb
index 16947f4d3be..a675014a77b 100644
--- a/spec/controllers/snippets_controller_spec.rb
+++ b/spec/controllers/snippets_controller_spec.rb
@@ -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
diff --git a/spec/features/issues/issue_detail_spec.rb b/spec/features/issues/issue_detail_spec.rb
index 0d24b02a64c..3bb70fdf376 100644
--- a/spec/features/issues/issue_detail_spec.rb
+++ b/spec/features/issues/issue_detail_spec.rb
@@ -28,7 +28,7 @@ describe 'Issue Detail', :js do
visit project_issue_path(project, issue)
end
- it 'encodes the description to prevent xss issues' do
+ it 'encodes the description to prevent xss issues', quarantine: 'https://gitlab.com/gitlab-org/gitlab/issues/207951' do
page.within('.issuable-details .detail-page-description') do
image = find('img.js-lazy-loaded')
diff --git a/spec/features/merge_request/user_creates_merge_request_spec.rb b/spec/features/merge_request/user_creates_merge_request_spec.rb
index 67f6d8ebe32..86ee9fa5aa5 100644
--- a/spec/features/merge_request/user_creates_merge_request_spec.rb
+++ b/spec/features/merge_request/user_creates_merge_request_spec.rb
@@ -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) }
diff --git a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
index d3a0c9b790b..9d9c83331fb 100644
--- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
describe 'User updates wiki page' do
+ include WikiHelpers
+
let(:user) { create(:user) }
before do
@@ -11,8 +13,11 @@ describe 'User updates wiki page' do
end
context 'when wiki is empty' do
- before do
+ before do |example|
visit(project_wikis_path(project))
+
+ wait_for_svg_to_be_loaded(example)
+
click_link "Create your first page"
end
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/project.json b/spec/fixtures/lib/gitlab/import_export/complex/project.json
index fae02540868..72ed8b818d8 100644
--- a/spec/fixtures/lib/gitlab/import_export/complex/project.json
+++ b/spec/fixtures/lib/gitlab/import_export/complex/project.json
@@ -6452,7 +6452,7 @@
]
},
{
- "id": 37,
+ "id": 26,
"project_id": 5,
"ref": "master",
"sha": "048721d90c449b244b7b4c53a9186b04330174ec",
@@ -6744,7 +6744,7 @@
]
},
{
- "id": 40,
+ "id": 19,
"project_id": 5,
"ref": "master",
"sha": "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73",
@@ -6851,7 +6851,7 @@
]
},
{
- "id": 42,
+ "id": 20,
"project_id": 5,
"ref": "master",
"sha": "ce84140e8b878ce6e7c4d298c7202ff38170e3ac",
diff --git a/spec/frontend/error_tracking/components/error_details_spec.js b/spec/frontend/error_tracking/components/error_details_spec.js
index e43f9569ffc..2baec5f37fb 100644
--- a/spec/frontend/error_tracking/components/error_details_spec.js
+++ b/spec/frontend/error_tracking/components/error_details_spec.js
@@ -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({
diff --git a/spec/graphql/types/diff_refs_type_spec.rb b/spec/graphql/types/diff_refs_type_spec.rb
index 91017c827ad..85225e5809c 100644
--- a/spec/graphql/types/diff_refs_type_spec.rb
+++ b/spec/graphql/types/diff_refs_type_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/asset_proxy_spec.rb b/spec/lib/gitlab/asset_proxy_spec.rb
new file mode 100644
index 00000000000..f5aa1819982
--- /dev/null
+++ b/spec/lib/gitlab/asset_proxy_spec.rb
@@ -0,0 +1,50 @@
+# 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
diff --git a/spec/lib/gitlab/background_migration/recalculate_project_authorizations_with_min_max_user_id_spec.rb b/spec/lib/gitlab/background_migration/recalculate_project_authorizations_with_min_max_user_id_spec.rb
new file mode 100644
index 00000000000..14ba57eecbf
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/recalculate_project_authorizations_with_min_max_user_id_spec.rb
@@ -0,0 +1,38 @@
+# 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
diff --git a/spec/lib/gitlab/dependency_linker/base_linker_spec.rb b/spec/lib/gitlab/dependency_linker/base_linker_spec.rb
new file mode 100644
index 00000000000..1466ce2dfcc
--- /dev/null
+++ b/spec/lib/gitlab/dependency_linker/base_linker_spec.rb
@@ -0,0 +1,53 @@
+# 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
diff --git a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
index a46c6579670..7bc17b804df 100644
--- a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
@@ -104,6 +104,24 @@ describe Gitlab::ImportExport::Project::TreeRestorer do
expect(pipeline.merge_request.source_branch).to eq('feature_conflict')
end
+ it 'restores pipelines based on ascending id order' do
+ expected_ordered_shas = %w[
+ 2ea1f3dec713d940208fb5ce4a38765ecb5d3f73
+ ce84140e8b878ce6e7c4d298c7202ff38170e3ac
+ 048721d90c449b244b7b4c53a9186b04330174ec
+ sha-notes
+ 5f923865dde3436854e9ceb9cdb7815618d4e849
+ d2d430676773caa88cdaf7c55944073b2fd5561a
+ 2ea1f3dec713d940208fb5ce4a38765ecb5d3f73
+ ]
+
+ project = Project.find_by_path('project')
+
+ project.ci_pipelines.order(:id).each_with_index do |pipeline, i|
+ expect(pipeline['sha']).to eq expected_ordered_shas[i]
+ end
+ end
+
it 'preserves updated_at on issues' do
issue = Issue.where(description: 'Aliquam enim illo et possimus.').first
@@ -385,7 +403,7 @@ describe Gitlab::ImportExport::Project::TreeRestorer do
it 'has the correct number of pipelines and statuses' do
expect(@project.ci_pipelines.size).to eq(7)
- @project.ci_pipelines.order(:id).zip([2, 2, 2, 2, 2, 0, 0])
+ @project.ci_pipelines.order(:id).zip([2, 0, 2, 2, 2, 2, 0])
.each do |(pipeline, expected_status_size)|
expect(pipeline.statuses.size).to eq(expected_status_size)
end
@@ -422,7 +440,7 @@ describe Gitlab::ImportExport::Project::TreeRestorer do
end
it 'restores external pull request for the restored pipeline' do
- pipeline_with_external_pr = @project.ci_pipelines.order(:id).last
+ pipeline_with_external_pr = @project.ci_pipelines.where(source: 'external_pull_request_event').first
expect(pipeline_with_external_pr.external_pull_request).to be_persisted
end
diff --git a/spec/lib/gitlab/project_authorizations_spec.rb b/spec/lib/gitlab/project_authorizations_spec.rb
index 1c579128223..7b282433061 100644
--- a/spec/lib/gitlab/project_authorizations_spec.rb
+++ b/spec/lib/gitlab/project_authorizations_spec.rb
@@ -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 }
diff --git a/spec/lib/gitlab/user_access_spec.rb b/spec/lib/gitlab/user_access_spec.rb
index 8d13f377677..78370f0136c 100644
--- a/spec/lib/gitlab/user_access_spec.rb
+++ b/spec/lib/gitlab/user_access_spec.rb
@@ -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) }
diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb
index 48fc2d826bc..d3780d22241 100644
--- a/spec/lib/gitlab/utils_spec.rb
+++ b/spec/lib/gitlab/utils_spec.rb
@@ -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
diff --git a/spec/migrations/clean_grafana_url_spec.rb b/spec/migrations/clean_grafana_url_spec.rb
new file mode 100644
index 00000000000..9f060fbaf7d
--- /dev/null
+++ b/spec/migrations/clean_grafana_url_spec.rb
@@ -0,0 +1,37 @@
+# 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
diff --git a/spec/migrations/schedule_recalculate_project_authorizations_second_run_spec.rb b/spec/migrations/schedule_recalculate_project_authorizations_second_run_spec.rb
new file mode 100644
index 00000000000..04726f98c89
--- /dev/null
+++ b/spec/migrations/schedule_recalculate_project_authorizations_second_run_spec.rb
@@ -0,0 +1,28 @@
+# 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
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 82ece6b2b91..9dad6b1e766 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -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
diff --git a/spec/models/badge_spec.rb b/spec/models/badge_spec.rb
index 60ae579eb03..fba8f40e99b 100644
--- a/spec/models/badge_spec.rb
+++ b/spec/models/badge_spec.rb
@@ -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
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index b5ed29189fd..576ac4393ed 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -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
diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb
index ad7dfac87af..9b5cce1aebf 100644
--- a/spec/models/members/group_member_spec.rb
+++ b/spec/models/members/group_member_spec.rb
@@ -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
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index cbd55077316..56f4c68a913 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -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) }
diff --git a/spec/models/user_detail_spec.rb b/spec/models/user_detail_spec.rb
index 3b6de5bb390..2b2bfff7be2 100644
--- a/spec/models/user_detail_spec.rb
+++ b/spec/models/user_detail_spec.rb
@@ -7,7 +7,7 @@ describe UserDetail do
describe 'validations' do
describe 'job_title' do
- it { is_expected.to validate_presence_of(:job_title) }
+ it { is_expected.not_to validate_presence_of(:job_title) }
it { is_expected.to validate_length_of(:job_title).is_at_most(200) }
end
end
diff --git a/spec/presenters/ci/pipeline_presenter_spec.rb b/spec/presenters/ci/pipeline_presenter_spec.rb
index 700d1f5cbb6..c9c4f567549 100644
--- a/spec/presenters/ci/pipeline_presenter_spec.rb
+++ b/spec/presenters/ci/pipeline_presenter_spec.rb
@@ -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
diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb
index 0309618e243..bcc1c6bc4d4 100644
--- a/spec/requests/api/triggers_spec.rb
+++ b/spec/requests/api/triggers_spec.rb
@@ -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
diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb
index 5003dfcc951..84f4a7a4e7a 100644
--- a/spec/services/auth/container_registry_authentication_service_spec.rb
+++ b/spec/services/auth/container_registry_authentication_service_spec.rb
@@ -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
diff --git a/spec/services/groups/group_links/destroy_service_spec.rb b/spec/services/groups/group_links/destroy_service_spec.rb
index 6f49b6eda94..284bcd0df2e 100644
--- a/spec/services/groups/group_links/destroy_service_spec.rb
+++ b/spec/services/groups/group_links/destroy_service_spec.rb
@@ -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
diff --git a/spec/services/groups/group_links/update_service_spec.rb b/spec/services/groups/group_links/update_service_spec.rb
new file mode 100644
index 00000000000..446364c9799
--- /dev/null
+++ b/spec/services/groups/group_links/update_service_spec.rb
@@ -0,0 +1,59 @@
+# 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
diff --git a/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb
index 21a139cdf3c..496d1fe67f2 100644
--- a/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb
+++ b/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb
@@ -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
diff --git a/spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb
index 9dac29765a2..e94d8a85987 100644
--- a/spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb
+++ b/spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb
@@ -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")
diff --git a/spec/support/shared_contexts/policies/group_policy_shared_context.rb b/spec/support/shared_contexts/policies/group_policy_shared_context.rb
index 63ebbcb93f9..3a306f80b3c 100644
--- a/spec/support/shared_contexts/policies/group_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/group_policy_shared_context.rb
@@ -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
diff --git a/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb
index 73087befad2..662c64647d6 100644
--- a/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb
@@ -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
diff --git a/spec/uploaders/file_mover_spec.rb b/spec/uploaders/file_mover_spec.rb
index 9f053f03b0e..474515b537c 100644
--- a/spec/uploaders/file_mover_spec.rb
+++ b/spec/uploaders/file_mover_spec.rb
@@ -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
diff --git a/spec/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb
index beb7aa3cf2c..efdbea85d4a 100644
--- a/spec/uploaders/file_uploader_spec.rb
+++ b/spec/uploaders/file_uploader_spec.rb
@@ -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
diff --git a/spec/uploaders/personal_file_uploader_spec.rb b/spec/uploaders/personal_file_uploader_spec.rb
index ec652af222d..c211ec3607c 100644
--- a/spec/uploaders/personal_file_uploader_spec.rb
+++ b/spec/uploaders/personal_file_uploader_spec.rb
@@ -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',
diff --git a/spec/validators/addressable_url_validator_spec.rb b/spec/validators/addressable_url_validator_spec.rb
index e8a44f7a12a..46b1bebb074 100644
--- a/spec/validators/addressable_url_validator_spec.rb
+++ b/spec/validators/addressable_url_validator_spec.rb
@@ -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) }