diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-01-28 15:52:04 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-01-28 15:52:04 +0000 |
commit | 62f3248f57987c736a4b3ea39a042e8ab220e5c8 (patch) | |
tree | 9e64978e5d407079f07944ec799779debd566695 | |
parent | 586bb7dc9629714cb1a46e358213063a6a48761b (diff) | |
download | gitlab-ce-62f3248f57987c736a4b3ea39a042e8ab220e5c8.tar.gz |
Add latest changes from gitlab-org/security/gitlab@12-5-stable-ee
90 files changed, 1293 insertions, 191 deletions
@@ -64,7 +64,7 @@ gem 'u2f', '~> 0.2.1' # GitLab Pages gem 'validates_hostname', '~> 1.0.6' -gem 'rubyzip', '~> 1.3.0', require: 'zip' +gem 'rubyzip', '~> 2.0.0', require: 'zip' # GitLab Pages letsencrypt support gem 'acme-client', '~> 2.0.2' @@ -141,7 +141,7 @@ gem 'gitlab-markup', '~> 1.7.0' gem 'github-markup', '~> 1.7.0', require: 'github/markup' gem 'commonmarker', '~> 0.17' gem 'RedCloth', '~> 4.3.2' -gem 'rdoc', '~> 6.0' +gem 'rdoc', '~> 6.1.2' gem 'org-ruby', '~> 0.9.12' gem 'creole', '~> 0.5.0' gem 'wikicloth', '0.8.1' diff --git a/Gemfile.lock b/Gemfile.lock index 469bea9d4fb..dafe501e686 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -262,7 +262,7 @@ GEM et-orbi (1.2.1) tzinfo eventmachine (1.2.7) - excon (0.62.0) + excon (0.71.1) execjs (2.6.0) expression_parser (0.9.0) extended-markdown-filter (0.6.0) @@ -760,7 +760,8 @@ GEM rack (>= 0.4) rack-attack (6.2.0) rack (>= 1.0, < 3) - rack-cors (1.0.2) + rack-cors (1.0.6) + rack (>= 1.6.0) rack-oauth2 (1.9.3) activesupport attr_required @@ -817,7 +818,7 @@ GEM ffi (>= 1.0.6) msgpack (>= 0.4.3) optimist (>= 3.0.0) - rdoc (6.0.4) + rdoc (6.1.2) re2 (1.1.1) recaptcha (4.13.1) json @@ -926,7 +927,7 @@ GEM sexp_processor (~> 4.9) rubyntlm (0.6.2) rubypants (0.2.0) - rubyzip (1.3.0) + rubyzip (2.0.0) rugged (0.28.3.1) safe_yaml (1.0.4) sanitize (4.6.6) @@ -1295,7 +1296,7 @@ DEPENDENCIES raindrops (~> 0.18) rblineprof (~> 0.3.6) rbtrace (~> 0.4) - rdoc (~> 6.0) + rdoc (~> 6.1.2) re2 (~> 1.1.1) recaptcha (~> 4.11) redis (~> 4.0) @@ -1319,7 +1320,7 @@ DEPENDENCIES ruby-prof (~> 1.0.0) ruby-progressbar ruby_parser (~> 3.8) - rubyzip (~> 1.3.0) + rubyzip (~> 2.0.0) rugged (~> 0.28) sanitize (~> 4.6) sassc-rails (~> 2.1.0) diff --git a/app/assets/javascripts/frequent_items/components/app.vue b/app/assets/javascripts/frequent_items/components/app.vue index 968e255e1fc..2a19752122d 100644 --- a/app/assets/javascripts/frequent_items/components/app.vue +++ b/app/assets/javascripts/frequent_items/components/app.vue @@ -5,7 +5,7 @@ import { GlLoadingIcon } from '@gitlab/ui'; import eventHub from '../event_hub'; import store from '../store/'; import { FREQUENT_ITEMS, STORAGE_KEY } from '../constants'; -import { isMobile, updateExistingFrequentItem } from '../utils'; +import { isMobile, updateExistingFrequentItem, sanitizeItem } from '../utils'; import FrequentItemsSearchInput from './frequent_items_search_input.vue'; import FrequentItemsList from './frequent_items_list.vue'; import frequentItemsMixin from './frequent_items_mixin'; @@ -64,7 +64,9 @@ export default { this.fetchFrequentItems(); } }, - logItemAccess(storageKey, item) { + logItemAccess(storageKey, unsanitizedItem) { + const item = sanitizeItem(unsanitizedItem); + if (!AccessorUtilities.isLocalStorageAccessSafe()) { return false; } diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_list.vue b/app/assets/javascripts/frequent_items/components/frequent_items_list.vue index 67ffa97a046..0ece64692ae 100644 --- a/app/assets/javascripts/frequent_items/components/frequent_items_list.vue +++ b/app/assets/javascripts/frequent_items/components/frequent_items_list.vue @@ -1,6 +1,7 @@ <script> import FrequentItemsListItem from './frequent_items_list_item.vue'; import frequentItemsMixin from './frequent_items_mixin'; +import { sanitizeItem } from '../utils'; export default { components: { @@ -48,6 +49,9 @@ export default { ? this.translations.itemListErrorMessage : this.translations.itemListEmptyMessage; }, + sanitizedItems() { + return this.items.map(sanitizeItem); + }, }, }; </script> @@ -59,7 +63,7 @@ export default { {{ listEmptyMessage }} </li> <frequent-items-list-item - v-for="item in items" + v-for="item in sanitizedItems" v-else :key="item.id" :item-id="item.id" diff --git a/app/assets/javascripts/frequent_items/utils.js b/app/assets/javascripts/frequent_items/utils.js index aba692e4b99..09d88d34223 100644 --- a/app/assets/javascripts/frequent_items/utils.js +++ b/app/assets/javascripts/frequent_items/utils.js @@ -1,5 +1,6 @@ import _ from 'underscore'; import bp from '~/breakpoints'; +import sanitize from 'sanitize-html'; import { FREQUENT_ITEMS, HOUR_IN_MS } from './constants'; export const isMobile = () => { @@ -47,3 +48,9 @@ export const updateExistingFrequentItem = (frequentItem, item) => { lastAccessedOn: accessedOverHourAgo ? Date.now() : frequentItem.lastAccessedOn, }; }; + +export const sanitizeItem = item => ({ + ...item, + name: sanitize(item.name.toString(), { allowedTags: [] }), + namespace: sanitize(item.namespace.toString(), { allowedTags: [] }), +}); diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js index a5e38022b8d..4daa8c60e58 100644 --- a/app/assets/javascripts/groups_select.js +++ b/app/assets/javascripts/groups_select.js @@ -1,6 +1,7 @@ import $ from 'jquery'; import axios from './lib/utils/axios_utils'; import Api from './api'; +import { escape } from 'lodash'; import { normalizeHeaders } from './lib/utils/common_utils'; import { __ } from '~/locale'; @@ -75,10 +76,12 @@ const groupsSelect = () => { } }, formatResult(object) { - return `<div class='group-result'> <div class='group-name'>${object.full_name}</div> <div class='group-path'>${object.full_path}</div> </div>`; + return `<div class='group-result'> <div class='group-name'>${escape( + object.full_name, + )}</div> <div class='group-path'>${object.full_path}</div> </div>`; }, formatSelection(object) { - return object.full_name; + return escape(object.full_name); }, dropdownCssClass: 'ajax-groups-dropdown select2-infinite', // we do not want to escape markup since we are displaying html in results diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 25c1d80b117..e904d03e33d 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -5,6 +5,7 @@ require 'fogbugz' class ApplicationController < ActionController::Base include Gitlab::GonHelper + include Gitlab::NoCacheHeaders include GitlabRoutingHelper include PageLayoutHelper include SafeParamsHelper @@ -54,7 +55,6 @@ class ApplicationController < ActionController::Base # Adds `no-store` to the DEFAULT_CACHE_CONTROL, to prevent security # concerns due to caching private data. DEFAULT_GITLAB_CACHE_CONTROL = "#{ActionDispatch::Http::Cache::Response::DEFAULT_CACHE_CONTROL}, no-store" - DEFAULT_GITLAB_CONTROL_NO_CACHE = "#{DEFAULT_GITLAB_CACHE_CONTROL}, no-cache" rescue_from Encoding::CompatibilityError do |exception| log_exception(exception) @@ -238,9 +238,9 @@ class ApplicationController < ActionController::Base end def no_cache_headers - headers['Cache-Control'] = DEFAULT_GITLAB_CONTROL_NO_CACHE - headers['Pragma'] = 'no-cache' # HTTP 1.0 compatibility - headers['Expires'] = 'Fri, 01 Jan 1990 00:00:00 GMT' + DEFAULT_GITLAB_NO_CACHE_HEADERS.each do |k, v| + headers[k] = v + end end def default_headers diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 1a97b39d3ae..1668cf004f8 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -19,7 +19,7 @@ class DashboardController < Dashboard::ApplicationController format.json do load_events - pager_json("events/_events", @events.count) + pager_json('events/_events', @events.count { |event| event.visible_to_user?(current_user) }) end end end @@ -37,6 +37,7 @@ class DashboardController < Dashboard::ApplicationController @events = EventCollection .new(projects, offset: params[:offset].to_i, filter: event_filter) .to_a + .map(&:present) Events::RenderService.new(current_user).execute(@events) end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 755d97b091c..3ffe406f5be 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -91,7 +91,7 @@ class GroupsController < Groups::ApplicationController format.json do load_events - pager_json("events/_events", @events.count) + pager_json("events/_events", @events.count { |event| event.visible_to_user?(current_user) }) end end end @@ -208,8 +208,9 @@ class GroupsController < Groups::ApplicationController .includes(:namespace) @events = EventCollection - .new(projects, offset: params[:offset].to_i, filter: event_filter, groups: groups) - .to_a + .new(projects, offset: params[:offset].to_i, filter: event_filter, groups: groups) + .to_a + .map(&:present) Events::RenderService .new(current_user) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index e5dea031bb5..5931e3484bf 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -116,7 +116,7 @@ class ProjectsController < Projects::ApplicationController format.html format.json do load_events - pager_json('events/_events', @events.count) + pager_json('events/_events', @events.count { |event| event.visible_to_user?(current_user) }) end end end @@ -337,6 +337,7 @@ class ProjectsController < Projects::ApplicationController @events = EventCollection .new(projects, offset: params[:offset].to_i, filter: event_filter) .to_a + .map(&:present) Events::RenderService.new(current_user).execute(@events, atom_request: request.format.atom?) end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index c68b6bdea0f..7f720916842 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -365,8 +365,8 @@ module ProjectsHelper @project.grafana_integration&.grafana_url end - def grafana_integration_token - @project.grafana_integration&.token + def grafana_integration_masked_token + @project.grafana_integration&.masked_token end def grafana_integration_enabled? diff --git a/app/models/generic_commit_status.rb b/app/models/generic_commit_status.rb index 8a768b3a2c0..8bba90c7da4 100644 --- a/app/models/generic_commit_status.rb +++ b/app/models/generic_commit_status.rb @@ -1,11 +1,14 @@ # frozen_string_literal: true class GenericCommitStatus < CommitStatus + EXTERNAL_STAGE_IDX = 1_000_000 + before_validation :set_default_values validates :target_url, addressable_url: true, length: { maximum: 255 }, allow_nil: true + validate :name_uniqueness_across_types, unless: :importing? # GitHub compatible API alias_attribute :context, :name @@ -13,7 +16,7 @@ class GenericCommitStatus < CommitStatus def set_default_values self.context ||= 'default' self.stage ||= 'external' - self.stage_idx ||= 1000000 + self.stage_idx ||= EXTERNAL_STAGE_IDX end def tags @@ -25,4 +28,14 @@ class GenericCommitStatus < CommitStatus .new(self, current_user) .fabricate! end + + private + + def name_uniqueness_across_types + return if !pipeline || name.blank? + + if pipeline.statuses.where(name: name).where.not(type: type).exists? + errors.add(:name, :taken) + end + end end diff --git a/app/models/grafana_integration.rb b/app/models/grafana_integration.rb index ed4c279965a..00213732fee 100644 --- a/app/models/grafana_integration.rb +++ b/app/models/grafana_integration.rb @@ -8,11 +8,13 @@ class GrafanaIntegration < ApplicationRecord algorithm: 'aes-256-gcm', key: Settings.attr_encrypted_db_key_base_32 + before_validation :check_token_changes + validates :grafana_url, length: { maximum: 1024 }, addressable_url: { enforce_sanitization: true, ascii_only: true } - validates :token, :project, presence: true + validates :encrypted_token, :project, presence: true validates :enabled, inclusion: { in: [true, false] } @@ -23,4 +25,28 @@ class GrafanaIntegration < ApplicationRecord @client ||= ::Grafana::Client.new(api_url: grafana_url.chomp('/'), token: token) end + + def masked_token + mask(encrypted_token) + end + + def masked_token_was + mask(encrypted_token_was) + end + + private + + def token + decrypt(:token, encrypted_token) + end + + def check_token_changes + return unless [encrypted_token_was, masked_token_was].include?(token) + + clear_attribute_changes [:token, :encrypted_token, :encrypted_token_iv] + end + + def mask(token) + token&.squish&.gsub(/./, '*') + end end diff --git a/app/models/note.rb b/app/models/note.rb index 493132e30cc..afd1cf5a5bc 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -537,7 +537,8 @@ class Note < ApplicationRecord # if they are not equal, then there are private/confidential references as well user_visible_reference_count > 0 && user_visible_reference_count == total_reference_count else - referenced_mentionables(user).any? + refs = all_references(user) + refs.all.any? && refs.stateful_not_visible_counter == 0 end end diff --git a/app/models/project.rb b/app/models/project.rb index 7ae4e2a4cd7..a5d01e95501 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -2274,6 +2274,10 @@ class Project < ApplicationRecord end end + def template_source? + false + end + private def closest_namespace_setting(name) diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb index 8f5c6957a20..fc89f74ada4 100644 --- a/app/policies/base_policy.rb +++ b/app/policies/base_policy.rb @@ -21,6 +21,14 @@ class BasePolicy < DeclarativePolicy::Base with_options scope: :user, score: 0 condition(:deactivated) { @user&.deactivated? } + desc "User email is unconfirmed or user account is locked" + with_options scope: :user, score: 0 + condition(:inactive) do + Feature.enabled?(:inactive_policy_condition, default_enabled: true) && + @user && + !@user&.active_for_authentication? + end + with_options scope: :user, score: 0 condition(:external_user) { @user.nil? || @user.external? } diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb index eca73f0a241..c0cb5843f97 100644 --- a/app/policies/global_policy.rb +++ b/app/policies/global_policy.rb @@ -36,6 +36,13 @@ class GlobalPolicy < BasePolicy enable :use_slash_commands end + rule { inactive }.policy do + prevent :log_in + prevent :access_api + prevent :access_git + prevent :use_slash_commands + end + rule { blocked | internal }.policy do prevent :log_in prevent :access_api diff --git a/app/presenters/event_presenter.rb b/app/presenters/event_presenter.rb index f31d362d5fa..5657e0b96bc 100644 --- a/app/presenters/event_presenter.rb +++ b/app/presenters/event_presenter.rb @@ -3,6 +3,18 @@ class EventPresenter < Gitlab::View::Presenter::Delegated presents :event + def initialize(subject, **attributes) + super + + @visible_to_user_cache = ActiveSupport::Cache::MemoryStore.new + end + + # Caching `visible_to_user?` method in the presenter beause it might be called multiple times. + def visible_to_user?(user = nil) + @visible_to_user_cache.fetch(user&.id) { super(user) } + end + + # implement cache here def resource_parent_name resource_parent&.full_name || '' end diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb index 3f0aedfbfb2..569b91de73e 100644 --- a/app/services/compare_service.rb +++ b/app/services/compare_service.rb @@ -18,7 +18,7 @@ class CompareService return unless raw_compare && raw_compare.base && raw_compare.head Compare.new(raw_compare, - target_project, + start_project, base_sha: base_sha, straight: straight) end diff --git a/app/services/projects/group_links/destroy_service.rb b/app/services/projects/group_links/destroy_service.rb index c96dcaae8d5..ea7d05551fd 100644 --- a/app/services/projects/group_links/destroy_service.rb +++ b/app/services/projects/group_links/destroy_service.rb @@ -6,6 +6,12 @@ module Projects def execute(group_link) return false unless group_link + if group_link.project.private? + TodosDestroyer::ProjectPrivateWorker.perform_in(Todo::WAIT_FOR_DELETE, project.id) + else + TodosDestroyer::ConfidentialIssueWorker.perform_in(Todo::WAIT_FOR_DELETE, nil, project.id) + end + group_link.destroy end end diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb index 8344397f67d..38859c1efa4 100644 --- a/app/services/projects/import_export/export_service.rb +++ b/app/services/projects/import_export/export_service.rb @@ -4,6 +4,12 @@ module Projects module ImportExport class ExportService < BaseService def execute(after_export_strategy = nil, options = {}) + unless project.template_source? || can?(current_user, :admin_project, project) + raise ::Gitlab::ImportExport::Error.new( + "User with ID: %s does not have permission to Project %s with ID: %s." % + [current_user.id, project.name, project.id]) + end + @shared = project.import_export_shared save_all! diff --git a/app/views/projects/settings/operations/_grafana_integration.html.haml b/app/views/projects/settings/operations/_grafana_integration.html.haml index cd5b5abd9ce..69e42a6c4fb 100644 --- a/app/views/projects/settings/operations/_grafana_integration.html.haml +++ b/app/views/projects/settings/operations/_grafana_integration.html.haml @@ -1,2 +1,2 @@ .js-grafana-integration{ data: { operations_settings_endpoint: project_settings_operations_path(@project), - grafana_integration: { url: grafana_integration_url, token: grafana_integration_token, enabled: grafana_integration_enabled?.to_s } } } + grafana_integration: { url: grafana_integration_url, token: grafana_integration_masked_token, enabled: grafana_integration_enabled?.to_s } } } diff --git a/changelogs/unreleased/7597-add-template-repository-usage-to-the-usage-ping.yml b/changelogs/unreleased/7597-add-template-repository-usage-to-the-usage-ping.yml new file mode 100644 index 00000000000..f9479c3eef4 --- /dev/null +++ b/changelogs/unreleased/7597-add-template-repository-usage-to-the-usage-ping.yml @@ -0,0 +1,5 @@ +--- +title: Add template repository usage to the usage ping +merge_request: 20126 +author: minghuan lei +type: changed diff --git a/changelogs/unreleased/jl-bump-rack-cors-1-0-6.yml b/changelogs/unreleased/jl-bump-rack-cors-1-0-6.yml new file mode 100644 index 00000000000..d54a7d885d1 --- /dev/null +++ b/changelogs/unreleased/jl-bump-rack-cors-1-0-6.yml @@ -0,0 +1,5 @@ +--- +title: Update rack-cors to 1.0.6 +merge_request: +author: +type: security diff --git a/changelogs/unreleased/jl-bump-rdoc-6-1-2.yml b/changelogs/unreleased/jl-bump-rdoc-6-1-2.yml new file mode 100644 index 00000000000..69c37e121a5 --- /dev/null +++ b/changelogs/unreleased/jl-bump-rdoc-6-1-2.yml @@ -0,0 +1,5 @@ +--- +title: Update rdoc to 6.1.2 +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-13-update-ruby-zip-pages-master.yml b/changelogs/unreleased/security-13-update-ruby-zip-pages-master.yml new file mode 100644 index 00000000000..976ce6f90b3 --- /dev/null +++ b/changelogs/unreleased/security-13-update-ruby-zip-pages-master.yml @@ -0,0 +1,5 @@ +--- +title: Bump rubyzip to 2.0.0 +merge_request: +author: Utkarsh Gupta +type: security diff --git a/changelogs/unreleased/security-35235-todos-cleanup.yml b/changelogs/unreleased/security-35235-todos-cleanup.yml new file mode 100644 index 00000000000..119220fbc73 --- /dev/null +++ b/changelogs/unreleased/security-35235-todos-cleanup.yml @@ -0,0 +1,5 @@ +--- +title: Cleanup todos for users from a removed linked group +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-commits-api-last-pipeline-status.yml b/changelogs/unreleased/security-commits-api-last-pipeline-status.yml new file mode 100644 index 00000000000..a68151f9732 --- /dev/null +++ b/changelogs/unreleased/security-commits-api-last-pipeline-status.yml @@ -0,0 +1,5 @@ +--- +title: Disable access to last_pipeline in commits API for users without read permissions +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-dependency-proxy-path-traversal.yml b/changelogs/unreleased/security-dependency-proxy-path-traversal.yml new file mode 100644 index 00000000000..ca0a03e36ab --- /dev/null +++ b/changelogs/unreleased/security-dependency-proxy-path-traversal.yml @@ -0,0 +1,5 @@ +--- +title: Add constraint to group dependency proxy endpoint param +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-dos-via-asciidoc-includes.yml b/changelogs/unreleased/security-dos-via-asciidoc-includes.yml new file mode 100644 index 00000000000..8fc3bd32316 --- /dev/null +++ b/changelogs/unreleased/security-dos-via-asciidoc-includes.yml @@ -0,0 +1,5 @@ +--- +title: Limit number of AsciiDoc includes per document +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-email-confirmation-bypass-via-api-ee.yml b/changelogs/unreleased/security-email-confirmation-bypass-via-api-ee.yml new file mode 100644 index 00000000000..8bd2b7a452f --- /dev/null +++ b/changelogs/unreleased/security-email-confirmation-bypass-via-api-ee.yml @@ -0,0 +1,5 @@ +--- +title: Prevent API access for unconfirmed users +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-enforce-permissions-for-event-filter-ee.yml b/changelogs/unreleased/security-enforce-permissions-for-event-filter-ee.yml new file mode 100644 index 00000000000..7d74d6108f8 --- /dev/null +++ b/changelogs/unreleased/security-enforce-permissions-for-event-filter-ee.yml @@ -0,0 +1,5 @@ +--- +title: Enforce permission check when counting activity events +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-fix-grafana-token-leaked-in-plain-to-other-maintainers.yml b/changelogs/unreleased/security-fix-grafana-token-leaked-in-plain-to-other-maintainers.yml new file mode 100644 index 00000000000..a44005f8dac --- /dev/null +++ b/changelogs/unreleased/security-fix-grafana-token-leaked-in-plain-to-other-maintainers.yml @@ -0,0 +1,5 @@ +--- +title: Prevent gafana integration token from being displayed as a plain text to other project maintainers, by only displaying a masked version of it. +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-fix-xss-on-frequent-groups-dropdown.yml b/changelogs/unreleased/security-fix-xss-on-frequent-groups-dropdown.yml new file mode 100644 index 00000000000..970708fe8d5 --- /dev/null +++ b/changelogs/unreleased/security-fix-xss-on-frequent-groups-dropdown.yml @@ -0,0 +1,5 @@ +--- +title: Fix xss on frequent groups dropdown +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-fix-xss-on-project-templates.yml b/changelogs/unreleased/security-fix-xss-on-project-templates.yml new file mode 100644 index 00000000000..2930bbaff87 --- /dev/null +++ b/changelogs/unreleased/security-fix-xss-on-project-templates.yml @@ -0,0 +1,5 @@ +--- +title: Fix XSS vulnerability on custom project templates form +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-proctect-internal-builds-from-external-overrides.yml b/changelogs/unreleased/security-proctect-internal-builds-from-external-overrides.yml new file mode 100644 index 00000000000..b540172d95c --- /dev/null +++ b/changelogs/unreleased/security-proctect-internal-builds-from-external-overrides.yml @@ -0,0 +1,5 @@ +--- +title: Protect internal CI builds from external overrides +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-project_export_service_permission_check.yml b/changelogs/unreleased/security-project_export_service_permission_check.yml new file mode 100644 index 00000000000..a38aaabfc9b --- /dev/null +++ b/changelogs/unreleased/security-project_export_service_permission_check.yml @@ -0,0 +1,5 @@ +--- +title: ImportExport::ExportService to require admin_project permission +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-reference-check.yml b/changelogs/unreleased/security-reference-check.yml new file mode 100644 index 00000000000..f33cea66eb1 --- /dev/null +++ b/changelogs/unreleased/security-reference-check.yml @@ -0,0 +1,5 @@ +--- +title: Make sure that only system notes where all references are visible to user are exposed in GraphQL API. +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-remove-caching-from-api-project-raw-endpoint.yml b/changelogs/unreleased/security-remove-caching-from-api-project-raw-endpoint.yml new file mode 100644 index 00000000000..308a618da89 --- /dev/null +++ b/changelogs/unreleased/security-remove-caching-from-api-project-raw-endpoint.yml @@ -0,0 +1,5 @@ +--- +title: Disable caching of repository/files/:file_path/raw API endpoint +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-reverse-polarity-of-branch-compare.yml b/changelogs/unreleased/security-reverse-polarity-of-branch-compare.yml new file mode 100644 index 00000000000..db6a4f064a4 --- /dev/null +++ b/changelogs/unreleased/security-reverse-polarity-of-branch-compare.yml @@ -0,0 +1,5 @@ +--- +title: Make cross-repository comparisons happen in the source repository +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-update-excon-cve-2019-16779.yml b/changelogs/unreleased/security-update-excon-cve-2019-16779.yml new file mode 100644 index 00000000000..e849dc92848 --- /dev/null +++ b/changelogs/unreleased/security-update-excon-cve-2019-16779.yml @@ -0,0 +1,5 @@ +--- +title: Update excon to 0.71.1 to fix CVE-2019-16779 +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-workhorse-package-bypass-12-5.yml b/changelogs/unreleased/security-workhorse-package-bypass-12-5.yml new file mode 100644 index 00000000000..bb9aa0a2bf1 --- /dev/null +++ b/changelogs/unreleased/security-workhorse-package-bypass-12-5.yml @@ -0,0 +1,5 @@ +--- +title: Add workhorse request verification to package upload endpoints +merge_request: +author: +type: security diff --git a/doc/user/asciidoc.md b/doc/user/asciidoc.md index b4d3cb58e97..da6bf287955 100644 --- a/doc/user/asciidoc.md +++ b/doc/user/asciidoc.md @@ -221,6 +221,11 @@ include::basics.adoc[] include::https://example.org/installation.adoc[] ``` +To guarantee good system performance and prevent malicious documents causing +problems, GitLab enforces a **maximum limit** on the number of include directives +processed in any one document. Currently a total of 32 documents can be +included, a number that is inclusive of transitive dependencies. + ### Blocks ```asciidoc diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb index d108c811f4b..e0a6dc41b65 100644 --- a/lib/api/commit_statuses.rb +++ b/lib/api/commit_statuses.rb @@ -85,6 +85,8 @@ module API protected: @project.protected_for?(ref)) end + authorize! :update_pipeline, pipeline + status = GenericCommitStatus.running_or_pending.find_or_initialize_by( project: @project, pipeline: pipeline, diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 63a7fdfa3ab..9dcf9b015aa 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -154,7 +154,7 @@ module API not_found! 'Commit' unless commit - present commit, with: Entities::CommitDetail, stats: params[:stats] + present commit, with: Entities::CommitDetail, stats: params[:stats], current_user: current_user end desc 'Get the diff for a specific commit of a project' do diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 32a0fb9dd60..694c48f644a 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -456,8 +456,18 @@ module API class CommitDetail < Commit expose :stats, using: Entities::CommitStats, if: :stats expose :status - expose :last_pipeline, using: 'API::Entities::PipelineBasic' expose :project_id + + expose :last_pipeline do |commit, options| + pipeline = commit.last_pipeline if can_read_pipeline? + ::API::Entities::PipelineBasic.represent(pipeline, options) + end + + private + + def can_read_pipeline? + Ability.allowed?(options[:current_user], :read_pipeline, object.last_pipeline) + end end class CommitSignature < Grape::Entity diff --git a/lib/api/files.rb b/lib/api/files.rb index 0b438fb5bbc..feed22d188c 100644 --- a/lib/api/files.rb +++ b/lib/api/files.rb @@ -127,6 +127,7 @@ module API get ":id/repository/files/:file_path/raw", requirements: FILE_ENDPOINT_REQUIREMENTS do assign_file_vars! + no_cache_headers set_http_headers(blob_data) send_git_blob @repo, @blob diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 49b86489a8b..4dcbd26775d 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -256,11 +256,19 @@ module API end def require_gitlab_workhorse! + verify_workhorse_api! + unless env['HTTP_GITLAB_WORKHORSE'].present? forbidden!('Request should be executed via GitLab Workhorse') end end + def verify_workhorse_api! + Gitlab::Workhorse.verify_api_request!(request.headers) + rescue + forbidden! + end + def require_pages_enabled! not_found! unless user_project.pages_available? end diff --git a/lib/api/helpers/headers_helpers.rb b/lib/api/helpers/headers_helpers.rb index 7553af9d156..908c57bb04e 100644 --- a/lib/api/helpers/headers_helpers.rb +++ b/lib/api/helpers/headers_helpers.rb @@ -3,6 +3,8 @@ module API module Helpers module HeadersHelpers + include Gitlab::NoCacheHeaders + def set_http_headers(header_data) header_data.each do |key, value| if value.is_a?(Enumerable) @@ -12,6 +14,12 @@ module API header "X-Gitlab-#{key.to_s.split('_').collect(&:capitalize).join('-')}", value.to_s end end + + def no_cache_headers + DEFAULT_GITLAB_NO_CACHE_HEADERS.each do |k, v| + header k, v + end + end end end end diff --git a/lib/banzai/reference_parser/base_parser.rb b/lib/banzai/reference_parser/base_parser.rb index 8419769085a..ffc6bdc612d 100644 --- a/lib/banzai/reference_parser/base_parser.rb +++ b/lib/banzai/reference_parser/base_parser.rb @@ -201,12 +201,14 @@ module Banzai gather_references(nodes) end - # Gathers the references for the given HTML nodes. + # Gathers the references for the given HTML nodes. Returns visible + # references and a list of nodes which are not visible to the user def gather_references(nodes) nodes = nodes_user_can_reference(current_user, nodes) - nodes = nodes_visible_to_user(current_user, nodes) + visible = nodes_visible_to_user(current_user, nodes) + not_visible = nodes - visible - referenced_by(nodes) + { visible: referenced_by(visible), not_visible: not_visible } end # Returns a Hash containing the projects for a given list of HTML nodes. diff --git a/lib/gitlab/asciidoc.rb b/lib/gitlab/asciidoc.rb index da65caa6c9c..8d072422e17 100644 --- a/lib/gitlab/asciidoc.rb +++ b/lib/gitlab/asciidoc.rb @@ -11,6 +11,7 @@ module Gitlab # the resulting HTML through HTML pipeline filters. module Asciidoc MAX_INCLUDE_DEPTH = 5 + MAX_INCLUDES = 32 DEFAULT_ADOC_ATTRS = { 'showtitle' => true, 'sectanchors' => true, @@ -40,6 +41,7 @@ module Gitlab extensions: extensions } context[:pipeline] = :ascii_doc + context[:max_includes] = [MAX_INCLUDES, context[:max_includes]].compact.min plantuml_setup diff --git a/lib/gitlab/asciidoc/include_processor.rb b/lib/gitlab/asciidoc/include_processor.rb index c6fbf540e9c..c3dca21b4b5 100644 --- a/lib/gitlab/asciidoc/include_processor.rb +++ b/lib/gitlab/asciidoc/include_processor.rb @@ -13,7 +13,9 @@ module Gitlab super(logger: Gitlab::AppLogger) @context = context - @repository = context[:project].try(:repository) + @repository = context[:repository] || context[:project].try(:repository) + @max_includes = context[:max_includes].to_i + @included = [] # Note: Asciidoctor calls #freeze on extensions, so we can't set new # instance variables after initialization. @@ -28,8 +30,11 @@ module Gitlab def include_allowed?(target, reader) doc = reader.document - return false if doc.attributes.fetch('max-include-depth').to_i < 1 + max_include_depth = doc.attributes.fetch('max-include-depth').to_i + + return false if max_include_depth < 1 return false if target_uri?(target) + return false if included.size >= max_includes true end @@ -62,7 +67,7 @@ module Gitlab private - attr_accessor :context, :repository, :cache + attr_reader :context, :repository, :cache, :max_includes, :included # Gets a Blob at a path for a specific revision. # This method will check that the Blob exists and contains readable text. @@ -77,6 +82,8 @@ module Gitlab raise 'Blob not found' unless blob raise 'File is not readable' unless blob.readable_text? + included << filename + blob end diff --git a/lib/gitlab/git/cross_repo_comparer.rb b/lib/gitlab/git/cross_repo_comparer.rb new file mode 100644 index 00000000000..3958373f7cb --- /dev/null +++ b/lib/gitlab/git/cross_repo_comparer.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +module Gitlab + module Git + class CrossRepoComparer + attr_reader :source_repo, :target_repo + + def initialize(source_repo, target_repo) + @source_repo = source_repo + @target_repo = target_repo + end + + def compare(source_ref, target_ref, straight:) + ensuring_ref_in_source(target_ref) do |target_commit_id| + Gitlab::Git::Compare.new( + source_repo, + target_commit_id, + source_ref, + straight: straight + ) + end + end + + private + + def ensuring_ref_in_source(ref, &blk) + return yield ref if source_repo == target_repo + + # If the commit doesn't exist in the target, there's nothing we can do + commit_id = target_repo.commit(ref)&.sha + return unless commit_id + + # The commit pointed to by ref may exist in the source even when they + # are different repositories. This is particularly true of close forks, + # but may also be the case if a temporary ref for this comparison has + # already been created in the past, and the result hasn't been GC'd yet. + return yield commit_id if source_repo.commit(commit_id) + + # Worst case: the commit is not in the source repo so we need to fetch + # it. Use a temporary ref and clean up afterwards + with_commit_in_source_tmp(commit_id, &blk) + end + + # Fetch the ref into source_repo from target_repo, using a temporary ref + # name that will be deleted once the method completes. This is a no-op if + # fetching the source branch fails + def with_commit_in_source_tmp(commit_id, &blk) + tmp_ref = "refs/tmp/#{SecureRandom.hex}" + + yield commit_id if source_repo.fetch_source_branch!(target_repo, commit_id, tmp_ref) + ensure + source_repo.delete_refs(tmp_ref) # best-effort + end + end + end +end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 4971a18e270..ea44a2f40c0 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -746,29 +746,9 @@ module Gitlab end def compare_source_branch(target_branch_name, source_repository, source_branch_name, straight:) - reachable_ref = - if source_repository == self - source_branch_name - else - # If a tmp ref was created before for a separate repo comparison (forks), - # we're able to short-circuit the tmp ref re-creation: - # 1. Take the SHA from the source repo - # 2. Read that in the current "target" repo - # 3. If that SHA is still known (readable), it means GC hasn't - # cleaned it up yet, so we can use it instead re-writing the tmp ref. - source_commit_id = source_repository.commit(source_branch_name)&.sha - commit(source_commit_id)&.sha if source_commit_id - end - - return compare(target_branch_name, reachable_ref, straight: straight) if reachable_ref - - tmp_ref = "refs/tmp/#{SecureRandom.hex}" - - return unless fetch_source_branch!(source_repository, source_branch_name, tmp_ref) - - compare(target_branch_name, tmp_ref, straight: straight) - ensure - delete_refs(tmp_ref) if tmp_ref + CrossRepoComparer + .new(source_repository, self) + .compare(source_branch_name, target_branch_name, straight: straight) end def write_ref(ref_path, ref, old_ref: nil) @@ -1045,13 +1025,6 @@ module Gitlab private - def compare(base_ref, head_ref, straight:) - Gitlab::Git::Compare.new(self, - base_ref, - head_ref, - straight: straight) - end - def empty_diff_stats Gitlab::Git::DiffStatsCollection.new([]) end diff --git a/lib/gitlab/no_cache_headers.rb b/lib/gitlab/no_cache_headers.rb new file mode 100644 index 00000000000..f80ca2c1369 --- /dev/null +++ b/lib/gitlab/no_cache_headers.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Gitlab + module NoCacheHeaders + DEFAULT_GITLAB_NO_CACHE_HEADERS = { + 'Cache-Control' => "#{ActionDispatch::Http::Cache::Response::DEFAULT_CACHE_CONTROL}, no-store, no-cache", + 'Pragma' => 'no-cache', # HTTP 1.0 compatibility + 'Expires' => 'Fri, 01 Jan 1990 00:00:00 GMT' + }.freeze + + def no_cache_headers + raise "#no_cache_headers is not implemented for this object" + end + end +end diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb index ea2b03b42c1..48276465efd 100644 --- a/lib/gitlab/reference_extractor.rb +++ b/lib/gitlab/reference_extractor.rb @@ -6,11 +6,16 @@ module Gitlab REFERABLES = %i(user issue label milestone merge_request snippet commit commit_range directly_addressed_user epic).freeze attr_accessor :project, :current_user, :author + # This counter is increased by a number of references filtered out by + # banzai reference exctractor. Note that this counter is stateful and + # not idempotent and is increased whenever you call `references`. + attr_reader :stateful_not_visible_counter def initialize(project, current_user = nil) @project = project @current_user = current_user @references = {} + @stateful_not_visible_counter = 0 super() end @@ -20,11 +25,15 @@ module Gitlab end def references(type) - super(type, project, current_user) + refs = super(type, project, current_user) + @stateful_not_visible_counter += refs[:not_visible].count + + refs[:visible] end def reset_memoized_values @references = {} + @stateful_not_visible_counter = 0 super() end diff --git a/package.json b/package.json index 016f4f96e21..15fa8438787 100644 --- a/package.json +++ b/package.json @@ -99,6 +99,7 @@ "jszip": "^3.1.3", "jszip-utils": "^0.0.2", "katex": "^0.10.0", + "lodash": "^4.17.15", "marked": "^0.3.12", "mermaid": "^8.4.2", "monaco-editor": "^0.15.6", diff --git a/qa/qa/specs/features/api/3_create/repository/files_spec.rb b/qa/qa/specs/features/api/3_create/repository/files_spec.rb index f6f020da472..dc471128dae 100644 --- a/qa/qa/specs/features/api/3_create/repository/files_spec.rb +++ b/qa/qa/specs/features/api/3_create/repository/files_spec.rb @@ -59,5 +59,48 @@ module QA a_hash_including(message: '202 Accepted') ) end + + describe 'raw file access' do + let(:svg_file) do + <<-SVG + <?xml version="1.0" standalone="no"?> + <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> + + <svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg"> + <polygon id="triangle" points="0,0 0,50 50,0" fill="#009900" stroke="#004400"/> + <script type="text/javascript"> + alert("surprise"); + </script> + </svg> + SVG + end + + it 'sets no-cache headers as expected' do + create_project_request = Runtime::API::Request.new(@api_client, '/projects') + post create_project_request.url, path: project_name, name: project_name + + create_file_request = Runtime::API::Request.new(@api_client, "/projects/#{sanitized_project_path}/repository/files/test.svg") + post create_file_request.url, branch: 'master', content: svg_file, commit_message: 'Add test.svg' + + get_file_request = Runtime::API::Request.new(@api_client, "/projects/#{sanitized_project_path}/repository/files/test.svg/raw", ref: 'master') + + 3.times do + response = get get_file_request.url + + # Subsequent responses aren't cached, so headers should match from + # request to request, especially a 200 response rather than a 304 + # (indicating a cached response.) Further, :content_disposition + # should include `attachment` for all responses. + # + expect(response.headers[:cache_control]).to include("no-store") + expect(response.headers[:cache_control]).to include("no-cache") + expect(response.headers[:pragma]).to eq("no-cache") + expect(response.headers[:expires]).to eq("Fri, 01 Jan 1990 00:00:00 GMT") + expect(response.headers[:content_disposition]).to include("attachment") + expect(response.headers[:content_disposition]).not_to include("inline") + expect(response.headers[:content_type]).to include("image/svg+xml") + end + end + end end end diff --git a/spec/controllers/dashboard_controller_spec.rb b/spec/controllers/dashboard_controller_spec.rb index a733c3ecaa1..305419efe96 100644 --- a/spec/controllers/dashboard_controller_spec.rb +++ b/spec/controllers/dashboard_controller_spec.rb @@ -23,6 +23,47 @@ describe DashboardController do end end + describe "GET activity as JSON" do + render_views + + let(:user) { create(:user) } + let(:project) { create(:project, :public, issues_access_level: ProjectFeature::PRIVATE) } + + before do + create(:event, :created, project: project, target: create(:issue)) + + sign_in(user) + + request.cookies[:event_filter] = 'all' + end + + context 'when user has permission to see the event' do + before do + project.add_developer(user) + end + + it 'returns count' do + get :activity, params: { format: :json } + + expect(json_response['count']).to eq(1) + end + end + + context 'when user has no permission to see the event' do + it 'filters out invisible event' do + get :activity, params: { format: :json } + + expect(json_response['html']).to include(_('No activities found')) + end + + it 'filters out invisible event when calculating the count' do + get :activity, params: { format: :json } + + expect(json_response['count']).to eq(0) + end + end + end + it_behaves_like 'authenticates sessionless user', :issues, :atom, author_id: User.first it_behaves_like 'authenticates sessionless user', :issues_calendar, :ics diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index 2ed2b319298..ddfd2b424e7 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -47,7 +47,7 @@ describe GroupsController do it 'assigns events for all the projects in the group', :sidekiq_might_not_need_inline do subject - expect(assigns(:events)).to contain_exactly(event) + expect(assigns(:events).map(&:id)).to contain_exactly(event.id) end end end @@ -119,12 +119,12 @@ describe GroupsController do describe 'GET #activity' do render_views - before do - sign_in(user) - project - end - context 'as json' do + before do + sign_in(user) + project + end + it 'includes events from all projects in group and subgroups', :sidekiq_might_not_need_inline do 2.times do project = create(:project, group: group) @@ -141,6 +141,31 @@ describe GroupsController do expect(assigns(:projects).limit_value).to be_nil end end + + context 'when user has no permission to see the event' do + let(:user) { create(:user) } + let(:group) { create(:group) } + let(:project) { create(:project, group: group) } + + let(:project_with_restricted_access) do + create(:project, :public, issues_access_level: ProjectFeature::PRIVATE, group: group) + end + + before do + create(:event, project: project) + create(:event, :created, project: project_with_restricted_access, target: create(:issue)) + + group.add_guest(user) + + sign_in(user) + end + + it 'filters out invisible event' do + get :activity, params: { id: group.to_param }, format: :json + + expect(json_response['count']).to eq(1) + end + end end describe 'POST #create' do diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index d16201fff5a..a5317e6b413 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -64,6 +64,46 @@ describe ProjectsController do end end + describe "GET #activity as JSON" do + render_views + + let(:project) { create(:project, :public, issues_access_level: ProjectFeature::PRIVATE) } + + before do + create(:event, :created, project: project, target: create(:issue)) + + sign_in(user) + + request.cookies[:event_filter] = 'all' + end + + context 'when user has permission to see the event' do + before do + project.add_developer(user) + end + + it 'returns count' do + get :activity, params: { namespace_id: project.namespace, id: project, format: :json } + + expect(json_response['count']).to eq(1) + end + end + + context 'when user has no permission to see the event' do + it 'filters out invisible event' do + get :activity, params: { namespace_id: project.namespace, id: project, format: :json } + + expect(json_response['html']).to eq("\n") + end + + it 'filters out invisible event when calculating the count' do + get :activity, params: { namespace_id: project.namespace, id: project, format: :json } + + expect(json_response['count']).to eq(0) + end + end + end + describe "GET show" do context "user not project member" do before do diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index cd1b1f91e9f..3c65b02c1f6 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -928,14 +928,14 @@ describe ProjectsHelper do helper.instance_variable_set(:@project, project) end - subject { helper.grafana_integration_token } + subject { helper.grafana_integration_masked_token } it { is_expected.to eq(nil) } context 'grafana integration exists' do let!(:grafana_integration) { create(:grafana_integration, project: project) } - it { is_expected.to eq(grafana_integration.token) } + it { is_expected.to eq(grafana_integration.masked_token) } end end diff --git a/spec/javascripts/frequent_items/utils_spec.js b/spec/javascripts/frequent_items/utils_spec.js index cd27d79b29a..0c4d747d2d8 100644 --- a/spec/javascripts/frequent_items/utils_spec.js +++ b/spec/javascripts/frequent_items/utils_spec.js @@ -1,5 +1,10 @@ import bp from '~/breakpoints'; -import { isMobile, getTopFrequentItems, updateExistingFrequentItem } from '~/frequent_items/utils'; +import { + isMobile, + getTopFrequentItems, + updateExistingFrequentItem, + sanitizeItem, +} from '~/frequent_items/utils'; import { HOUR_IN_MS, FREQUENT_ITEMS } from '~/frequent_items/constants'; import { mockProject, unsortedFrequentItems, sortedFrequentItems } from './mock_data'; @@ -86,4 +91,16 @@ describe('Frequent Items utils spec', () => { expect(result.frequency).toBe(mockedProject.frequency); }); }); + + describe('sanitizeItem', () => { + it('strips HTML tags for name and namespace', () => { + const input = { + name: '<br><b>test</b>', + namespace: '<br>test', + id: 1, + }; + + expect(sanitizeItem(input)).toEqual({ name: 'test', namespace: 'test', id: 1 }); + }); + }); }); diff --git a/spec/lib/banzai/reference_parser/mentioned_user_parser_spec.rb b/spec/lib/banzai/reference_parser/mentioned_user_parser_spec.rb index 1be279375bd..b10e5d19828 100644 --- a/spec/lib/banzai/reference_parser/mentioned_user_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/mentioned_user_parser_spec.rb @@ -22,7 +22,7 @@ describe Banzai::ReferenceParser::MentionedUserParser do end it 'returns empty list of users' do - expect(subject.gather_references([link])).to eq([]) + expect_gathered_references(subject.gather_references([link]), [], 0) end end end @@ -35,7 +35,7 @@ describe Banzai::ReferenceParser::MentionedUserParser do end it 'returns empty list of users' do - expect(subject.gather_references([link])).to eq([]) + expect_gathered_references(subject.gather_references([link]), [], 0) end end end @@ -44,7 +44,7 @@ describe Banzai::ReferenceParser::MentionedUserParser do it 'returns an Array of users' do link['data-user'] = user.id.to_s - expect(subject.referenced_by([link])).to eq([user]) + expect_gathered_references(subject.gather_references([link]), [user], 0) end end end diff --git a/spec/lib/banzai/reference_parser/mentioned_users_by_group_parser_spec.rb b/spec/lib/banzai/reference_parser/mentioned_users_by_group_parser_spec.rb index 99d607629eb..48bded41f9c 100644 --- a/spec/lib/banzai/reference_parser/mentioned_users_by_group_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/mentioned_users_by_group_parser_spec.rb @@ -19,7 +19,7 @@ describe Banzai::ReferenceParser::MentionedUsersByGroupParser do it 'returns empty array' do link['data-group'] = project.group.id.to_s - expect(subject.gather_references([link])).to eq([]) + expect_gathered_references(subject.gather_references([link]), [], 1) end end @@ -30,7 +30,7 @@ describe Banzai::ReferenceParser::MentionedUsersByGroupParser do end it 'returns groups' do - expect(subject.gather_references([link])).to eq([group]) + expect_gathered_references(subject.gather_references([link]), [group], 0) end end @@ -38,7 +38,7 @@ describe Banzai::ReferenceParser::MentionedUsersByGroupParser do it 'returns an empty Array' do link['data-group'] = 'test-non-existing' - expect(subject.gather_references([link])).to eq([]) + expect_gathered_references(subject.gather_references([link]), [], 1) end end end diff --git a/spec/lib/banzai/reference_parser/mentioned_users_by_project_parser_spec.rb b/spec/lib/banzai/reference_parser/mentioned_users_by_project_parser_spec.rb index 155f2189d9e..574873711a7 100644 --- a/spec/lib/banzai/reference_parser/mentioned_users_by_project_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/mentioned_users_by_project_parser_spec.rb @@ -19,7 +19,7 @@ describe Banzai::ReferenceParser::MentionedUsersByProjectParser do it 'returns empty Array' do link['data-project'] = project.id.to_s - expect(subject.gather_references([link])).to eq([]) + expect_gathered_references(subject.gather_references([link]), [], 1) end end @@ -30,7 +30,7 @@ describe Banzai::ReferenceParser::MentionedUsersByProjectParser do end it 'returns an Array of referenced projects' do - expect(subject.gather_references([link])).to eq([project]) + expect_gathered_references(subject.gather_references([link]), [project], 0) end end @@ -38,7 +38,7 @@ describe Banzai::ReferenceParser::MentionedUsersByProjectParser do it 'returns an empty Array' do link['data-project'] = 'inexisting-project-id' - expect(subject.gather_references([link])).to eq([]) + expect_gathered_references(subject.gather_references([link]), [], 1) end end end diff --git a/spec/lib/banzai/reference_parser/project_parser_spec.rb b/spec/lib/banzai/reference_parser/project_parser_spec.rb index 356dde1e9c2..e87fa3e8767 100644 --- a/spec/lib/banzai/reference_parser/project_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/project_parser_spec.rb @@ -17,7 +17,7 @@ describe Banzai::ReferenceParser::ProjectParser do it 'returns an Array of projects' do link['data-project'] = project.id.to_s - expect(subject.gather_references([link])).to eq([project]) + expect_gathered_references(subject.gather_references([link]), [project], 0) end end @@ -25,7 +25,7 @@ describe Banzai::ReferenceParser::ProjectParser do it 'returns an empty Array' do link['data-project'] = '' - expect(subject.gather_references([link])).to eq([]) + expect_gathered_references(subject.gather_references([link]), [], 1) end end @@ -35,7 +35,7 @@ describe Banzai::ReferenceParser::ProjectParser do link['data-project'] = private_project.id.to_s - expect(subject.gather_references([link])).to eq([]) + expect_gathered_references(subject.gather_references([link]), [], 1) end it 'returns an Array when authorized' do @@ -43,7 +43,7 @@ describe Banzai::ReferenceParser::ProjectParser do link['data-project'] = private_project.id.to_s - expect(subject.gather_references([link])).to eq([private_project]) + expect_gathered_references(subject.gather_references([link]), [private_project], 0) end end end diff --git a/spec/lib/gitlab/asciidoc/include_processor_spec.rb b/spec/lib/gitlab/asciidoc/include_processor_spec.rb new file mode 100644 index 00000000000..72fa05939ae --- /dev/null +++ b/spec/lib/gitlab/asciidoc/include_processor_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'nokogiri' + +describe Gitlab::Asciidoc::IncludeProcessor do + let_it_be(:project) { create(:project, :repository) } + + let(:processor_context) do + { + project: project, + max_includes: max_includes, + ref: ref + } + end + let(:ref) { project.repository.root_ref } + let(:max_includes) { 10 } + + let(:reader) { Asciidoctor::PreprocessorReader.new(document, lines, 'file.adoc') } + let(:document) { Asciidoctor::Document.new(lines) } + + subject(:processor) { described_class.new(processor_context) } + + let(:a_blob) { double(:Blob, readable_text?: true, data: a_data) } + let(:a_data) { StringIO.new('include::b.adoc[]') } + + let(:lines) { [':max-include-depth: 1000'] + Array.new(10, 'include::a.adoc[]') } + + before do + allow(project.repository).to receive(:blob_at).with(ref, 'a.adoc').and_return(a_blob) + end + + describe '#include_allowed?' do + it 'allows the first include' do + expect(processor.send(:include_allowed?, 'foo.adoc', reader)).to be_truthy + end + + it 'allows the Nth + 1 include' do + (max_includes - 1).times { processor.send(:read_blob, ref, 'a.adoc') } + + expect(processor.send(:include_allowed?, 'foo.adoc', reader)).to be_truthy + end + + it 'disallows the Nth + 1 include' do + max_includes.times { processor.send(:read_blob, ref, 'a.adoc') } + + expect(processor.send(:include_allowed?, 'foo.adoc', reader)).to be_falsey + end + end +end diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb index 415a6e62374..e5bd8fba074 100644 --- a/spec/lib/gitlab/asciidoc_spec.rb +++ b/spec/lib/gitlab/asciidoc_spec.rb @@ -425,6 +425,24 @@ module Gitlab create_file(current_file, "= AsciiDoc\n") end + def many_includes(target) + Array.new(10, "include::#{target}[]").join("\n") + end + + context 'cyclic imports' do + before do + create_file('doc/api/a.adoc', many_includes('b.adoc')) + create_file('doc/api/b.adoc', many_includes('a.adoc')) + end + + let(:include_path) { 'a.adoc' } + let(:requested_path) { 'doc/api/README.md' } + + it 'completes successfully' do + is_expected.to include('<p>Include this:</p>') + end + end + context 'with path to non-existing file' do let(:include_path) { 'not-exists.adoc' } diff --git a/spec/lib/gitlab/git/cross_repo_comparer_spec.rb b/spec/lib/gitlab/git/cross_repo_comparer_spec.rb new file mode 100644 index 00000000000..8b37b6d1667 --- /dev/null +++ b/spec/lib/gitlab/git/cross_repo_comparer_spec.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Git::CrossRepoComparer do + let(:source_project) { create(:project, :repository) } + let(:target_project) { create(:project, :repository) } + + let(:source_repo) { source_project.repository.raw_repository } + let(:target_repo) { target_project.repository.raw_repository } + + let(:source_branch) { 'feature' } + let(:target_branch) { 'master' } + let(:straight) { false } + + let(:source_commit) { source_repo.commit(source_branch) } + let(:target_commit) { source_repo.commit(target_branch) } + + subject(:result) { described_class.new(source_repo, target_repo).compare(source_branch, target_branch, straight: straight) } + + describe '#compare' do + context 'within a single repository' do + let(:target_project) { source_project } + + context 'a non-straight comparison' do + it 'compares without fetching from another repo' do + expect(source_repo).not_to receive(:fetch_source_branch!) + + expect_compare(result, from: source_commit, to: target_commit) + expect(result.straight).to eq(false) + end + end + + context 'a straight comparison' do + let(:straight) { true } + + it 'compares without fetching from another repo' do + expect(source_repo).not_to receive(:fetch_source_branch!) + + expect_compare(result, from: source_commit, to: target_commit) + expect(result.straight).to eq(true) + end + end + end + + context 'across two repositories' do + context 'target ref exists in source repo' do + it 'compares without fetching from another repo' do + expect(source_repo).not_to receive(:fetch_source_branch!) + expect(source_repo).not_to receive(:delete_refs) + + expect_compare(result, from: source_commit, to: target_commit) + end + end + + context 'target ref does not exist in source repo' do + it 'compares in the source repo by fetching from the target to a temporary ref' do + new_commit_id = create_commit(target_project.owner, target_repo, target_branch) + new_commit = target_repo.commit(new_commit_id) + + # This is how the temporary ref is generated + expect(SecureRandom).to receive(:hex).at_least(:once).and_return('foo') + + expect(source_repo) + .to receive(:fetch_source_branch!) + .with(target_repo, new_commit_id, 'refs/tmp/foo') + .and_call_original + + expect(source_repo).to receive(:delete_refs).with('refs/tmp/foo').and_call_original + + expect_compare(result, from: source_commit, to: new_commit) + end + end + + context 'source ref does not exist in source repo' do + let(:source_branch) { 'does-not-exist' } + + it 'returns an empty comparison' do + expect(source_repo).not_to receive(:fetch_source_branch!) + expect(source_repo).not_to receive(:delete_refs) + + expect(result).to be_a(::Gitlab::Git::Compare) + expect(result.commits.size).to eq(0) + end + end + + context 'target ref does not exist in target repo' do + let(:target_branch) { 'does-not-exist' } + + it 'returns nil' do + expect(source_repo).not_to receive(:fetch_source_branch!) + expect(source_repo).not_to receive(:delete_refs) + + is_expected.to be_nil + end + end + end + end + + def expect_compare(of, from:, to:) + expect(of).to be_a(::Gitlab::Git::Compare) + expect(from).to be_a(::Gitlab::Git::Commit) + expect(to).to be_a(::Gitlab::Git::Commit) + + expect(of.commits).not_to be_empty + expect(of.head).to eq(from) + expect(of.base).to eq(to) + end + + def create_commit(user, repo, branch) + action = { action: :create, file_path: '/FILE', content: 'content' } + + result = repo.multi_action(user, branch_name: branch, message: 'Commit', actions: [action]) + + result.newrev + end +end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 44c41da7560..aa0110f257d 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1959,66 +1959,15 @@ describe Gitlab::Git::Repository, :seed_helper do end describe '#compare_source_branch' do - let(:repository) { Gitlab::Git::Repository.new('default', TEST_GITATTRIBUTES_REPO_PATH, '', 'group/project') } - - context 'within same repository' do - it 'does not create a temp ref' do - expect(repository).not_to receive(:fetch_source_branch!) - expect(repository).not_to receive(:delete_refs) - - compare = repository.compare_source_branch('master', repository, 'feature', straight: false) - expect(compare).to be_a(Gitlab::Git::Compare) - expect(compare.commits.count).to be > 0 - end - - it 'returns empty commits when source ref does not exist' do - compare = repository.compare_source_branch('master', repository, 'non-existent-branch', straight: false) + it 'delegates to Gitlab::Git::CrossRepoComparer' do + expect_next_instance_of(::Gitlab::Git::CrossRepoComparer) do |instance| + expect(instance.source_repo).to eq(:source_repository) + expect(instance.target_repo).to eq(repository) - expect(compare.commits).to be_empty + expect(instance).to receive(:compare).with('feature', 'master', straight: :straight) end - end - context 'with different repositories' do - context 'when ref is known by source repo, but not by target' do - before do - mutable_repository.write_ref('another-branch', 'feature') - end - - it 'creates temp ref' do - expect(repository).not_to receive(:fetch_source_branch!) - expect(repository).not_to receive(:delete_refs) - - compare = repository.compare_source_branch('master', mutable_repository, 'another-branch', straight: false) - expect(compare).to be_a(Gitlab::Git::Compare) - expect(compare.commits.count).to be > 0 - end - end - - context 'when ref is known by source and target repos' do - before do - mutable_repository.write_ref('another-branch', 'feature') - repository.write_ref('another-branch', 'feature') - end - - it 'does not create a temp ref' do - expect(repository).not_to receive(:fetch_source_branch!) - expect(repository).not_to receive(:delete_refs) - - compare = repository.compare_source_branch('master', mutable_repository, 'another-branch', straight: false) - expect(compare).to be_a(Gitlab::Git::Compare) - expect(compare.commits.count).to be > 0 - end - end - - context 'when ref is unknown by source repo' do - it 'returns nil when source ref does not exist' do - expect(repository).to receive(:fetch_source_branch!).and_call_original - expect(repository).to receive(:delete_refs).and_call_original - - compare = repository.compare_source_branch('master', mutable_repository, 'non-existent-branch', straight: false) - expect(compare).to be_nil - end - end + repository.compare_source_branch('master', :source_repository, 'feature', straight: :straight) end end diff --git a/spec/lib/gitlab/no_cache_headers_spec.rb b/spec/lib/gitlab/no_cache_headers_spec.rb new file mode 100644 index 00000000000..f011b55006e --- /dev/null +++ b/spec/lib/gitlab/no_cache_headers_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::NoCacheHeaders do + class NoCacheTester + include Gitlab::NoCacheHeaders + end + + describe "#no_cache_headers" do + subject { NoCacheTester.new } + + it "raises a RuntimeError" do + expect { subject.no_cache_headers }.to raise_error(RuntimeError) + end + end +end diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb index 6bc9b6365d1..0faaaa50621 100644 --- a/spec/lib/gitlab/reference_extractor_spec.rb +++ b/spec/lib/gitlab/reference_extractor_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Gitlab::ReferenceExtractor do - let(:project) { create(:project) } + let_it_be(:project) { create(:project) } before do project.add_developer(project.creator) @@ -293,4 +293,34 @@ describe Gitlab::ReferenceExtractor do end end end + + describe '#references' do + let_it_be(:user) { create(:user) } + let_it_be(:issue) { create(:issue, project: project) } + let(:text) { "Ref. #{issue.to_reference}" } + + subject { described_class.new(project, user) } + + before do + subject.analyze(text) + end + + context 'when references are visible' do + before do + project.add_developer(user) + end + + it 'returns visible references of given type' do + expect(subject.references(:issue)).to eq([issue]) + end + + it 'does not increase stateful_not_visible_counter' do + expect { subject.references(:issue) }.not_to change { subject.stateful_not_visible_counter } + end + end + + it 'increases stateful_not_visible_counter' do + expect { subject.references(:issue) }.to change { subject.stateful_not_visible_counter }.by(1) + end + end end diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb index c851810ffb3..c8ed898122b 100644 --- a/spec/models/generic_commit_status_spec.rb +++ b/spec/models/generic_commit_status_spec.rb @@ -19,6 +19,74 @@ describe GenericCommitStatus do it { is_expected.not_to allow_value('javascript:alert(1)').for(:target_url) } end + describe '#name_uniqueness_across_types' do + let(:attributes) { {} } + let(:commit_status) { described_class.new(attributes) } + let(:status_name) { 'test-job' } + + subject(:errors) { commit_status.errors[:name] } + + shared_examples 'it does not have uniqueness errors' do + it 'does not return errors' do + commit_status.valid? + + is_expected.to be_empty + end + end + + context 'without attributes' do + it_behaves_like 'it does not have uniqueness errors' + end + + context 'with only a pipeline' do + let(:attributes) { { pipeline: pipeline } } + + context 'without name' do + it_behaves_like 'it does not have uniqueness errors' + end + end + + context 'with only a name' do + let(:attributes) { { name: status_name } } + + context 'without pipeline' do + it_behaves_like 'it does not have uniqueness errors' + end + end + + context 'with pipeline and name' do + let(:attributes) do + { + pipeline: pipeline, + name: status_name + } + end + + context 'without other statuses' do + it_behaves_like 'it does not have uniqueness errors' + end + + context 'with generic statuses' do + before do + create(:generic_commit_status, pipeline: pipeline, name: status_name) + end + + it_behaves_like 'it does not have uniqueness errors' + end + + context 'with ci_build statuses' do + before do + create(:ci_build, pipeline: pipeline, name: status_name) + end + + it 'returns name error' do + expect(commit_status).to be_invalid + is_expected.to include('has already been taken') + end + end + end + end + describe '#context' do subject { generic_commit_status.context } @@ -79,6 +147,12 @@ describe GenericCommitStatus do it { is_expected.not_to be_nil } end + + describe '#stage_idx' do + subject { generic_commit_status.stage_idx } + + it { is_expected.not_to be_nil } + end end describe '#present' do diff --git a/spec/models/grafana_integration_spec.rb b/spec/models/grafana_integration_spec.rb index 615865e17b9..662e8b1dd61 100644 --- a/spec/models/grafana_integration_spec.rb +++ b/spec/models/grafana_integration_spec.rb @@ -9,7 +9,7 @@ describe GrafanaIntegration do describe 'validations' do it { is_expected.to validate_presence_of(:project) } - it { is_expected.to validate_presence_of(:token) } + it { is_expected.to validate_presence_of(:encrypted_token) } it 'disallows invalid urls for grafana_url' do unsafe_url = %{https://replaceme.com/'><script>alert(document.cookie)</script>} @@ -66,4 +66,24 @@ describe GrafanaIntegration do end end end + + describe 'attribute encryption' do + subject(:grafana_integration) { create(:grafana_integration, token: 'super-secret') } + + context 'token' do + it 'encrypts original value into encrypted_token attribute' do + expect(grafana_integration.encrypted_token).not_to be_nil + end + + it 'locks access to raw value in private method', :aggregate_failures do + expect { grafana_integration.token }.to raise_error(NoMethodError, /private method .token. called/) + expect(grafana_integration.send(:token)).to eql('super-secret') + end + + it 'prevents overriding token value with its encrypted or masked version', :aggregate_failures do + expect { grafana_integration.update(token: grafana_integration.encrypted_token) }.not_to change { grafana_integration.reload.send(:token) } + expect { grafana_integration.update(token: grafana_integration.masked_token) }.not_to change { grafana_integration.reload.send(:token) } + end + end + end end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 9c06cd049f5..c42a489b418 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -350,12 +350,12 @@ describe Note do end describe "cross_reference_not_visible_for?" do - let(:private_user) { create(:user) } - let(:private_project) { create(:project, namespace: private_user.namespace) { |p| p.add_maintainer(private_user) } } - let(:private_issue) { create(:issue, project: private_project) } + let_it_be(:private_user) { create(:user) } + let_it_be(:private_project) { create(:project, namespace: private_user.namespace) { |p| p.add_maintainer(private_user) } } + let_it_be(:private_issue) { create(:issue, project: private_project) } - let(:ext_proj) { create(:project, :public) } - let(:ext_issue) { create(:issue, project: ext_proj) } + let_it_be(:ext_proj) { create(:project, :public) } + let_it_be(:ext_issue) { create(:issue, project: ext_proj) } shared_examples "checks references" do it "returns true" do @@ -393,10 +393,24 @@ describe Note do it_behaves_like "checks references" end - context "when there are two references in note" do + context "when there is a reference to a label" do + let_it_be(:private_label) { create(:label, project: private_project) } let(:note) do create :note, noteable: ext_issue, project: ext_proj, + note: "added label #{private_label.to_reference(ext_proj)}", + system: true + end + let!(:system_note_metadata) { create(:system_note_metadata, note: note, action: :label) } + + it_behaves_like "checks references" + end + + context "when there are two references in note" do + let_it_be(:ext_issue2) { create(:issue, project: ext_proj) } + let(:note) do + create :note, + noteable: ext_issue2, project: ext_proj, note: "mentioned in issue #{private_issue.to_reference(ext_proj)} and " \ "public issue #{ext_issue.to_reference(ext_proj)}", system: true diff --git a/spec/policies/global_policy_spec.rb b/spec/policies/global_policy_spec.rb index c18cc245468..cea3d870263 100644 --- a/spec/policies/global_policy_spec.rb +++ b/spec/policies/global_policy_spec.rb @@ -141,6 +141,34 @@ describe GlobalPolicy do it { is_expected.to be_allowed(:access_api) } end end + + context 'inactive user' do + before do + current_user.update!(confirmed_at: nil, confirmation_sent_at: 5.days.ago) + end + + context 'when within the confirmation grace period' do + before do + allow(User).to receive(:allow_unconfirmed_access_for).and_return(10.days) + end + + it { is_expected.to be_allowed(:access_api) } + end + + context 'when confirmation grace period is expired' do + before do + allow(User).to receive(:allow_unconfirmed_access_for).and_return(2.days) + end + + it { is_expected.not_to be_allowed(:access_api) } + end + + it 'when `inactive_policy_condition` feature flag is turned off' do + stub_feature_flags(inactive_policy_condition: false) + + is_expected.to be_allowed(:access_api) + end + end end describe 'receive notifications' do @@ -202,6 +230,20 @@ describe GlobalPolicy do it { is_expected.not_to be_allowed(:access_git) } end + describe 'inactive user' do + before do + current_user.update!(confirmed_at: nil) + end + + it { is_expected.not_to be_allowed(:access_git) } + + it 'when `inactive_policy_condition` feature flag is turned off' do + stub_feature_flags(inactive_policy_condition: false) + + is_expected.to be_allowed(:access_git) + end + end + context 'when terms are enforced' do before do enforce_terms @@ -298,6 +340,20 @@ describe GlobalPolicy do it { is_expected.not_to be_allowed(:use_slash_commands) } end + describe 'inactive user' do + before do + current_user.update!(confirmed_at: nil) + end + + it { is_expected.not_to be_allowed(:use_slash_commands) } + + it 'when `inactive_policy_condition` feature flag is turned off' do + stub_feature_flags(inactive_policy_condition: false) + + is_expected.to be_allowed(:use_slash_commands) + end + end + context 'when access locked' do before do current_user.lock_access! diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb index 639b8e96343..24ed836996e 100644 --- a/spec/requests/api/commit_statuses_spec.rb +++ b/spec/requests/api/commit_statuses_spec.rb @@ -164,6 +164,7 @@ describe API::CommitStatuses do expect(response).to have_gitlab_http_status(201) expect(job.status).to eq('pending') + expect(job.stage_idx).to eq(GenericCommitStatus::EXTERNAL_STAGE_IDX) end end @@ -331,6 +332,29 @@ describe API::CommitStatuses do end end + context 'when updating a protected ref' do + before do + create(:protected_branch, project: project, name: 'master') + post api(post_url, user), params: { state: 'running', ref: 'master' } + end + + context 'with user as developer' do + let(:user) { developer } + + it 'does not create commit status' do + expect(response).to have_gitlab_http_status(403) + end + end + + context 'with user as maintainer' do + let(:user) { create_user(:maintainer) } + + it 'creates commit status' do + expect(response).to have_gitlab_http_status(201) + end + end + end + context 'when commit SHA is invalid' do let(:sha) { 'invalid_sha' } @@ -372,6 +396,22 @@ describe API::CommitStatuses do .to include 'is blocked: Only allowed schemes are http, https' end end + + context 'when trying to update a status of a different type' do + let!(:pipeline) { create(:ci_pipeline, project: project, sha: sha, ref: 'ref') } + let!(:ci_build) { create(:ci_build, pipeline: pipeline, name: 'test-job') } + let(:params) { { state: 'pending', name: 'test-job' } } + + before do + post api(post_url, developer), params: params + end + + it 'responds with bad request status and validation errors' do + expect(response).to have_gitlab_http_status(400) + expect(json_response['message']['name']) + .to include 'has already been taken' + end + end end context 'reporter user' do diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index d8da1c001b0..e390f3945a9 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -8,6 +8,7 @@ describe API::Commits do let(:user) { create(:user) } let(:guest) { create(:user).tap { |u| project.add_guest(u) } } + let(:developer) { create(:user).tap { |u| project.add_developer(u) } } let(:project) { create(:project, :repository, creator: user, path: 'my.project') } let(:branch_with_dot) { project.repository.find_branch('ends-with.json') } let(:branch_with_slash) { project.repository.find_branch('improve/awesome') } @@ -964,6 +965,56 @@ describe API::Commits do end end + shared_examples_for 'ref with pipeline' do + let!(:pipeline) do + project + .ci_pipelines + .create!(source: :push, ref: 'master', sha: commit.sha, protected: false) + end + + it 'includes status as "created" and a last_pipeline object' do + get api(route, current_user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/commit/detail') + expect(json_response['status']).to eq('created') + expect(json_response['last_pipeline']['id']).to eq(pipeline.id) + expect(json_response['last_pipeline']['ref']).to eq(pipeline.ref) + expect(json_response['last_pipeline']['sha']).to eq(pipeline.sha) + expect(json_response['last_pipeline']['status']).to eq(pipeline.status) + end + + context 'when pipeline succeeds' do + before do + pipeline.update!(status: 'success') + end + + it 'includes a "success" status' do + get api(route, current_user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/commit/detail') + expect(json_response['status']).to eq('success') + end + end + end + + shared_examples_for 'ref with unaccessible pipeline' do + let!(:pipeline) do + project + .ci_pipelines + .create!(source: :push, ref: 'master', sha: commit.sha, protected: false) + end + + it 'does not include last_pipeline' do + get api(route, current_user) + + expect(response).to match_response_schema('public_api/v4/commit/detail') + expect(response).to have_gitlab_http_status(200) + expect(json_response['last_pipeline']).to be_nil + end + end + context 'when stat param' do let(:route) { "/projects/#{project_id}/repository/commits/#{commit_id}" } @@ -993,6 +1044,15 @@ describe API::Commits do let(:project) { create(:project, :public, :repository) } it_behaves_like 'ref commit' + it_behaves_like 'ref with pipeline' + + context 'with private builds' do + before do + project.project_feature.update!(builds_access_level: ProjectFeature::PRIVATE) + end + + it_behaves_like 'ref with unaccessible pipeline' + end end context 'when unauthenticated', 'and project is private' do @@ -1006,6 +1066,17 @@ describe API::Commits do let(:current_user) { user } it_behaves_like 'ref commit' + it_behaves_like 'ref with pipeline' + + context 'when builds are disabled' do + before do + project + .project_feature + .update!(builds_access_level: ProjectFeature::DISABLED) + end + + it_behaves_like 'ref with unaccessible pipeline' + end context 'when branch contains a dot' do let(:commit) { project.repository.commit(branch_with_dot.name) } @@ -1041,35 +1112,53 @@ describe API::Commits do it_behaves_like 'ref commit' end end + end - context 'when the ref has a pipeline' do - let!(:pipeline) { project.ci_pipelines.create(source: :push, ref: 'master', sha: commit.sha, protected: false) } + context 'when authenticated', 'as a developer' do + let(:current_user) { developer } - it 'includes a "created" status' do - get api(route, current_user) + it_behaves_like 'ref commit' + it_behaves_like 'ref with pipeline' - expect(response).to have_gitlab_http_status(200) - expect(response).to match_response_schema('public_api/v4/commit/detail') - expect(json_response['status']).to eq('created') - expect(json_response['last_pipeline']['id']).to eq(pipeline.id) - expect(json_response['last_pipeline']['ref']).to eq(pipeline.ref) - expect(json_response['last_pipeline']['sha']).to eq(pipeline.sha) - expect(json_response['last_pipeline']['status']).to eq(pipeline.status) + context 'with private builds' do + before do + project.project_feature.update!(builds_access_level: ProjectFeature::PRIVATE) end - context 'when pipeline succeeds' do - before do - pipeline.update(status: 'success') - end + it_behaves_like 'ref with pipeline' + end + end - it 'includes a "success" status' do - get api(route, current_user) + context 'when authenticated', 'as a guest' do + let(:current_user) { guest } - expect(response).to have_gitlab_http_status(200) - expect(response).to match_response_schema('public_api/v4/commit/detail') - expect(json_response['status']).to eq('success') - end + it_behaves_like '403 response' do + let(:request) { get api(route, guest) } + let(:message) { '403 Forbidden' } + end + end + + context 'when authenticated', 'as a non member' do + let(:current_user) { create(:user) } + + it_behaves_like '403 response' do + let(:request) { get api(route, guest) } + let(:message) { '403 Forbidden' } + end + end + + context 'when authenticated', 'as non_member and project is public' do + let(:current_user) { create(:user) } + let(:project) { create(:project, :public, :repository) } + + it_behaves_like 'ref with pipeline' + + context 'with private builds' do + before do + project.project_feature.update!(builds_access_level: ProjectFeature::PRIVATE) end + + it_behaves_like 'ref with unaccessible pipeline' end end end diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index ec18156f49f..6b3b0b99dfc 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -447,6 +447,18 @@ describe API::Files do expect(response).to have_gitlab_http_status(200) end + it 'sets no-cache headers' do + url = route('.gitignore') + "/raw" + expect(Gitlab::Workhorse).to receive(:send_git_blob) + + get api(url, current_user), params: params + + expect(response.headers["Cache-Control"]).to include("no-store") + expect(response.headers["Cache-Control"]).to include("no-cache") + expect(response.headers["Pragma"]).to eq("no-cache") + expect(response.headers["Expires"]).to eq("Fri, 01 Jan 1990 00:00:00 GMT") + end + context 'when mandatory params are not given' do it_behaves_like '400 response' do let(:request) { get api(route("any%2Ffile"), current_user) } diff --git a/spec/requests/api/oauth_tokens_spec.rb b/spec/requests/api/oauth_tokens_spec.rb index 8d7b3fa3c09..ce03756a19a 100644 --- a/spec/requests/api/oauth_tokens_spec.rb +++ b/spec/requests/api/oauth_tokens_spec.rb @@ -30,26 +30,40 @@ describe 'OAuth tokens' do end end - context "when user is blocked" do - it "does not create an access token" do - user = create(:user) + shared_examples 'does not create an access token' do + let(:user) { create(:user) } + + it { expect(response).to have_gitlab_http_status(401) } + end + + context 'when user is blocked' do + before do user.block request_oauth_token(user) - - expect(response).to have_gitlab_http_status(401) end + + include_examples 'does not create an access token' end - context "when user is ldap_blocked" do - it "does not create an access token" do - user = create(:user) + context 'when user is ldap_blocked' do + before do user.ldap_block request_oauth_token(user) + end - expect(response).to have_gitlab_http_status(401) + include_examples 'does not create an access token' + end + + context 'when user account is not confirmed' do + before do + user.update!(confirmed_at: nil) + + request_oauth_token(user) end + + include_examples 'does not create an access token' end end end diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb index 6138036b0af..14f397bc050 100644 --- a/spec/requests/api/runner_spec.rb +++ b/spec/requests/api/runner_spec.rb @@ -1499,7 +1499,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do authorize_artifacts - expect(response).to have_gitlab_http_status(500) + expect(response).to have_gitlab_http_status(:forbidden) end context 'authorization token is invalid' do @@ -1629,6 +1629,16 @@ describe API::Runner, :clean_gitlab_redis_shared_state do end end + context 'Is missing GitLab Workhorse token headers' do + let(:jwt_token) { JWT.encode({ 'iss' => 'invalid-header' }, Gitlab::Workhorse.secret, 'HS256') } + + it 'fails to post artifacts without GitLab-Workhorse' do + upload_artifacts(file_upload, headers_with_token) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + context 'when setting an expire date' do let(:default_artifacts_expire_in) {} let(:post_data) do diff --git a/spec/services/projects/group_links/destroy_service_spec.rb b/spec/services/projects/group_links/destroy_service_spec.rb index d78ab78c3d8..0fd1fcfe1a5 100644 --- a/spec/services/projects/group_links/destroy_service_spec.rb +++ b/spec/services/projects/group_links/destroy_service_spec.rb @@ -3,8 +3,8 @@ require 'spec_helper' describe Projects::GroupLinks::DestroyService, '#execute' do - let(:group_link) { create :project_group_link } - let(:project) { group_link.project } + let(:project) { create(:project, :private) } + let!(:group_link) { create(:project_group_link, project: project) } let(:user) { create :user } let(:subject) { described_class.new(project, user) } @@ -15,4 +15,39 @@ describe Projects::GroupLinks::DestroyService, '#execute' do it 'returns false if group_link is blank' do expect { subject.execute(nil) }.not_to change { project.project_group_links.count } end + + describe 'todos cleanup' do + context 'when project is private' do + it 'triggers todos cleanup' do + expect(TodosDestroyer::ProjectPrivateWorker).to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, project.id) + expect(project.private?).to be true + + subject.execute(group_link) + end + end + + context 'when project is public or internal' do + shared_examples_for 'removes confidential todos' do + it 'does not trigger todos cleanup' do + expect(TodosDestroyer::ProjectPrivateWorker).not_to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, project.id) + expect(TodosDestroyer::ConfidentialIssueWorker).to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, nil, project.id) + expect(project.private?).to be false + + subject.execute(group_link) + end + end + + context 'when project is public' do + let(:project) { create(:project, :public) } + + it_behaves_like 'removes confidential todos' + end + + context 'when project is internal' do + let(:project) { create(:project, :public) } + + it_behaves_like 'removes confidential todos' + end + end + end end diff --git a/spec/services/projects/import_export/export_service_spec.rb b/spec/services/projects/import_export/export_service_spec.rb index a557e61da78..0069b73d310 100644 --- a/spec/services/projects/import_export/export_service_spec.rb +++ b/spec/services/projects/import_export/export_service_spec.rb @@ -10,6 +10,10 @@ describe Projects::ImportExport::ExportService do let(:service) { described_class.new(project, user) } let!(:after_export_strategy) { Gitlab::ImportExport::AfterExportStrategies::DownloadNotificationStrategy.new } + before do + project.add_maintainer(user) + end + it 'saves the version' do expect(Gitlab::ImportExport::VersionSaver).to receive(:new).and_call_original @@ -133,5 +137,18 @@ describe Projects::ImportExport::ExportService do expect(service).not_to receive(:execute_after_export_action) end end + + context 'when user does not have admin_project permission' do + let!(:another_user) { create(:user) } + + subject(:service) { described_class.new(project, another_user) } + + it 'fails' do + expected_message = + "User with ID: %s does not have permission to Project %s with ID: %s." % + [another_user.id, project.name, project.id] + expect { service.execute }.to raise_error(Gitlab::ImportExport::Error).with_message(expected_message) + end + end end end diff --git a/spec/services/projects/operations/update_service_spec.rb b/spec/services/projects/operations/update_service_spec.rb index 81d59a98b9b..e258c379bf8 100644 --- a/spec/services/projects/operations/update_service_spec.rb +++ b/spec/services/projects/operations/update_service_spec.rb @@ -210,7 +210,7 @@ describe Projects::Operations::UpdateService do integration = project.reload.grafana_integration expect(integration.grafana_url).to eq(expected_attrs[:grafana_url]) - expect(integration.token).to eq(expected_attrs[:token]) + expect(integration.send(:token)).to eq(expected_attrs[:token]) end end @@ -226,7 +226,7 @@ describe Projects::Operations::UpdateService do integration = project.reload.grafana_integration expect(integration.grafana_url).to eq(expected_attrs[:grafana_url]) - expect(integration.token).to eq(expected_attrs[:token]) + expect(integration.send(:token)).to eq(expected_attrs[:token]) end context 'with all grafana attributes blank in params' do diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb index fe92b53cd91..95970590347 100644 --- a/spec/services/projects/update_pages_service_spec.rb +++ b/spec/services/projects/update_pages_service_spec.rb @@ -5,7 +5,7 @@ require "spec_helper" describe Projects::UpdatePagesService do set(:project) { create(:project, :repository) } set(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit('HEAD').sha) } - set(:build) { create(:ci_build, pipeline: pipeline, ref: 'HEAD') } + let(:build) { create(:ci_build, pipeline: pipeline, ref: 'HEAD') } let(:invalid_file) { fixture_file_upload('spec/fixtures/dk.png') } let(:file) { fixture_file_upload("spec/fixtures/pages.zip") } @@ -242,6 +242,32 @@ describe Projects::UpdatePagesService do end end + context 'when file size is spoofed' do + let(:metadata) { spy('metadata') } + + include_context 'pages zip with spoofed size' + + before do + file = fixture_file_upload(fake_zip_path, 'pages.zip') + metafile = fixture_file_upload('spec/fixtures/pages.zip.meta') + + create(:ci_job_artifact, :archive, file: file, job: build) + create(:ci_job_artifact, :metadata, file: metafile, job: build) + + allow(build).to receive(:artifacts_metadata_entry) + .and_return(metadata) + allow(metadata).to receive(:total_size).and_return(100) + end + + it 'raises an error' do + expect do + subject.execute + end.to raise_error(Projects::UpdatePagesService::FailedToExtractError, + 'Entry public/index.html should be 1B but is larger when inflated') + expect(deploy_status).to be_script_failure + end + end + def deploy_status GenericCommitStatus.find_by(name: 'pages:deploy') end diff --git a/spec/support/helpers/reference_parser_helpers.rb b/spec/support/helpers/reference_parser_helpers.rb index f96a01d15b5..9084265b587 100644 --- a/spec/support/helpers/reference_parser_helpers.rb +++ b/spec/support/helpers/reference_parser_helpers.rb @@ -5,6 +5,11 @@ module ReferenceParserHelpers Nokogiri::HTML.fragment('<a></a>').children[0] end + def expect_gathered_references(result, visible, not_visible_count) + expect(result[:visible]).to eq(visible) + expect(result[:not_visible].count).to eq(not_visible_count) + end + shared_examples 'no project N+1 queries' do it 'avoids N+1 queries in #nodes_visible_to_user', :request_store do context = Banzai::RenderContext.new(project, user) diff --git a/spec/support/shared_contexts/pages_zip_with_spoofed_size_shared_context.rb b/spec/support/shared_contexts/pages_zip_with_spoofed_size_shared_context.rb new file mode 100644 index 00000000000..4cec5ab3b74 --- /dev/null +++ b/spec/support/shared_contexts/pages_zip_with_spoofed_size_shared_context.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +# the idea of creating zip archive with spoofed size is borrowed from +# https://github.com/rubyzip/rubyzip/pull/403/files#diff-118213fb4baa6404a40f89e1147661ebR88 +RSpec.shared_context 'pages zip with spoofed size' do + let(:real_zip_path) { Tempfile.new(['real', '.zip']).path } + let(:fake_zip_path) { Tempfile.new(['fake', '.zip']).path } + + before do + full_file_name = 'public/index.html' + true_size = 500_000 + fake_size = 1 + + ::Zip::File.open(real_zip_path, ::Zip::File::CREATE) do |zf| + zf.get_output_stream(full_file_name) do |os| + os.write 'a' * true_size + end + end + + compressed_size = nil + ::Zip::File.open(real_zip_path) do |zf| + a_entry = zf.find_entry(full_file_name) + compressed_size = a_entry.compressed_size + end + + true_size_bytes = [compressed_size, true_size, full_file_name.size].pack('LLS') + fake_size_bytes = [compressed_size, fake_size, full_file_name.size].pack('LLS') + + data = File.binread(real_zip_path) + data.gsub! true_size_bytes, fake_size_bytes + + File.open(fake_zip_path, 'wb') do |file| + file.write data + end + end + + after do + File.delete(real_zip_path) if File.exist?(real_zip_path) + File.delete(fake_zip_path) if File.exist?(fake_zip_path) + end +end diff --git a/vendor/gitignore/C++.gitignore b/vendor/gitignore/C++.gitignore index 259148fa18f..259148fa18f 100755..100644 --- a/vendor/gitignore/C++.gitignore +++ b/vendor/gitignore/C++.gitignore diff --git a/vendor/gitignore/Java.gitignore b/vendor/gitignore/Java.gitignore index a1c2a238a96..a1c2a238a96 100755..100644 --- a/vendor/gitignore/Java.gitignore +++ b/vendor/gitignore/Java.gitignore |