New upstream version 13.12.6+ds1

This commit is contained in:
Pirate Praveen 2021-07-02 01:05:55 +05:30
parent 3b6cf8664c
commit 3eea648134
78 changed files with 1033 additions and 308 deletions

View file

@ -2,6 +2,37 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 13.12.6 (2021-07-01)
### Added (1 change)
- [Added omniauth_user check when verifying user cap](gitlab-org/security/gitlab@a61062501630c35820301e9f79a036219d1e3074) ([merge request](gitlab-org/security/gitlab!1502)) **GitLab Enterprise Edition**
### Security (14 changes)
- [Bump rails gem version to 6.0.3.7](gitlab-org/security/gitlab@58d27ba819867baadf535e0d8d91d0cb818dc8b6) ([merge request](gitlab-org/security/gitlab!1515))
- [Update rdoc to 6.3.1](gitlab-org/security/gitlab@ead11a6974576b0b1a974985493c75143e3bd575) ([merge request](gitlab-org/security/gitlab!1534))
- [Add sanitizing for name field](gitlab-org/security/gitlab@2c5672eae4323c2682245485b327850e68e7e5b4) ([merge request](gitlab-org/security/gitlab!1490))
- [Forbid GET requests with mutations](gitlab-org/security/gitlab@2b01d6dc310451fa3022f1865470ca004bbd4c33) ([merge request](gitlab-org/security/gitlab!1529))
- [Copy feature visibility settings to a fork](gitlab-org/security/gitlab@5ee923ba64fb34fc38f831fc206a153d8f7eae91) ([merge request](gitlab-org/security/gitlab!1523))
- [Avoid disclosing project in web IDE](gitlab-org/security/gitlab@759d1361e7f359d681c4f55ea2b6f7e1d0bb1e53) ([merge request](gitlab-org/security/gitlab!1512))
- [Add new username validation](gitlab-org/security/gitlab@e79625541d04b0d6c94614f2afc6aaeb2ef40083) ([merge request](gitlab-org/security/gitlab!1495))
- [Allow only same-origin URLs for Edit Release Cancel button](gitlab-org/security/gitlab@e5bda0a7e03978afee494616e2054b8650b61d3e) ([merge request](gitlab-org/security/gitlab!1486))
- [Update Nokogiri to 1.11.4](gitlab-org/security/gitlab@d71973da1850df059b1ec1422d50bbccace21ff2) ([merge request](gitlab-org/security/gitlab!1479))
- [Fix deploy key fallback issue in protected branch](gitlab-org/security/gitlab@0411bc45885e1122c06dbff084b48bf03d78c6a8) ([merge request](gitlab-org/security/gitlab!1478))
- [Fix XSS on audit log for feature flag actions](gitlab-org/security/gitlab@22e2f903c821e54ce6d4b4b749a009d14abc4a13) ([merge request](gitlab-org/security/gitlab!1474))
- [Sanitize input on pasteGFM](gitlab-org/security/gitlab@7dc511ebc2e77c3d22cd34ca87449f32120a5229) ([merge request](gitlab-org/security/gitlab!1453))
- [Add total http read timeout](gitlab-org/security/gitlab@37c24c82d5dfa57fad03f265e7ba92f6ef250c30) ([merge request](gitlab-org/security/gitlab!1427))
- [Fix merge request diff display issue with unsupported encoding](gitlab-org/security/gitlab@7d05892daa6aaf951b941628e2af41e17977b140) ([merge request](gitlab-org/security/gitlab!1424))
## 13.12.5 (2021-06-21)
### Fixed (3 changes)
- [Fix failing spec](gitlab-org/gitlab@7d1a9b0155195eb082f5b33ba1310deed742a7a4) ([merge request](gitlab-org/gitlab!64488))
- [Advanced Search Settings page does not load if the ES url is unreachable](gitlab-org/gitlab@80b262f0e79f02a89724ed4e3988e686f53c959c) ([merge request](gitlab-org/gitlab!64488)) **GitLab Enterprise Edition**
- [Fix Password expired error on git fetch via SSH for LDAP user](gitlab-org/gitlab@19a7d7a6d3cd43f1c7559c729532ad3b9dafb75c) ([merge request](gitlab-org/gitlab!64488))
## 13.12.4 (2021-06-14)
### Fixed (3 changes)

View file

@ -1 +1 @@
13.12.4
13.12.6

View file

@ -2,7 +2,7 @@
source 'https://rubygems.org'
gem 'rails', '~> 6.0.3.6'
gem 'rails', '~> 6.0.3.7'
gem 'bootsnap', '~> 1.4.6'
@ -157,7 +157,7 @@ gem 'github-markup', '~> 1.7.0', require: 'github/markup'
gem 'commonmarker', '~> 0.21'
gem 'kramdown', '~> 2.3.1'
gem 'RedCloth', '~> 4.3.2'
gem 'rdoc', '~> 6.1.2'
gem 'gitlab-rdoc', '~> 6.3.2', require: 'rdoc' # We need this fork until rdoc releases a new version. See https://gitlab.com/gitlab-org/gitlab/-/issues/334695
gem 'org-ruby', '~> 0.9.12'
gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1'
@ -168,7 +168,7 @@ gem 'asciidoctor-kroki', '~> 0.4.0', require: false
gem 'rouge', '~> 3.26.0'
gem 'truncato', '~> 0.7.11'
gem 'bootstrap_form', '~> 4.2.0'
gem 'nokogiri', '~> 1.11.1'
gem 'nokogiri', '~> 1.11.4'
gem 'escape_utils', '~> 1.1'
# Calendar rendering

View file

@ -12,59 +12,59 @@ GEM
abstract_type (0.0.7)
acme-client (2.0.6)
faraday (>= 0.17, < 2.0.0)
actioncable (6.0.3.6)
actionpack (= 6.0.3.6)
actioncable (6.0.3.7)
actionpack (= 6.0.3.7)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (6.0.3.6)
actionpack (= 6.0.3.6)
activejob (= 6.0.3.6)
activerecord (= 6.0.3.6)
activestorage (= 6.0.3.6)
activesupport (= 6.0.3.6)
actionmailbox (6.0.3.7)
actionpack (= 6.0.3.7)
activejob (= 6.0.3.7)
activerecord (= 6.0.3.7)
activestorage (= 6.0.3.7)
activesupport (= 6.0.3.7)
mail (>= 2.7.1)
actionmailer (6.0.3.6)
actionpack (= 6.0.3.6)
actionview (= 6.0.3.6)
activejob (= 6.0.3.6)
actionmailer (6.0.3.7)
actionpack (= 6.0.3.7)
actionview (= 6.0.3.7)
activejob (= 6.0.3.7)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (6.0.3.6)
actionview (= 6.0.3.6)
activesupport (= 6.0.3.6)
actionpack (6.0.3.7)
actionview (= 6.0.3.7)
activesupport (= 6.0.3.7)
rack (~> 2.0, >= 2.0.8)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (6.0.3.6)
actionpack (= 6.0.3.6)
activerecord (= 6.0.3.6)
activestorage (= 6.0.3.6)
activesupport (= 6.0.3.6)
actiontext (6.0.3.7)
actionpack (= 6.0.3.7)
activerecord (= 6.0.3.7)
activestorage (= 6.0.3.7)
activesupport (= 6.0.3.7)
nokogiri (>= 1.8.5)
actionview (6.0.3.6)
activesupport (= 6.0.3.6)
actionview (6.0.3.7)
activesupport (= 6.0.3.7)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
activejob (6.0.3.6)
activesupport (= 6.0.3.6)
activejob (6.0.3.7)
activesupport (= 6.0.3.7)
globalid (>= 0.3.6)
activemodel (6.0.3.6)
activesupport (= 6.0.3.6)
activerecord (6.0.3.6)
activemodel (= 6.0.3.6)
activesupport (= 6.0.3.6)
activemodel (6.0.3.7)
activesupport (= 6.0.3.7)
activerecord (6.0.3.7)
activemodel (= 6.0.3.7)
activesupport (= 6.0.3.7)
activerecord-explain-analyze (0.1.0)
activerecord (>= 4)
pg
activestorage (6.0.3.6)
actionpack (= 6.0.3.6)
activejob (= 6.0.3.6)
activerecord (= 6.0.3.6)
activestorage (6.0.3.7)
actionpack (= 6.0.3.7)
activejob (= 6.0.3.7)
activerecord (= 6.0.3.7)
marcel (~> 1.0.0)
activesupport (6.0.3.6)
activesupport (6.0.3.7)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
@ -483,6 +483,7 @@ GEM
addressable (~> 2.7)
omniauth (~> 1.9)
openid_connect (~> 1.2)
gitlab-rdoc (6.3.2)
gitlab-sidekiq-fetcher (0.5.6)
sidekiq (~> 5)
gitlab-styles (6.2.0)
@ -781,7 +782,7 @@ GEM
netrc (0.11.0)
nio4r (2.5.4)
no_proxy_fix (0.1.2)
nokogiri (1.11.3)
nokogiri (1.11.4)
mini_portile2 (~> 2.5.0)
racc (~> 1.4)
nokogumbo (2.0.2)
@ -962,20 +963,20 @@ GEM
rack-test (1.1.0)
rack (>= 1.0, < 3)
rack-timeout (0.5.2)
rails (6.0.3.6)
actioncable (= 6.0.3.6)
actionmailbox (= 6.0.3.6)
actionmailer (= 6.0.3.6)
actionpack (= 6.0.3.6)
actiontext (= 6.0.3.6)
actionview (= 6.0.3.6)
activejob (= 6.0.3.6)
activemodel (= 6.0.3.6)
activerecord (= 6.0.3.6)
activestorage (= 6.0.3.6)
activesupport (= 6.0.3.6)
rails (6.0.3.7)
actioncable (= 6.0.3.7)
actionmailbox (= 6.0.3.7)
actionmailer (= 6.0.3.7)
actionpack (= 6.0.3.7)
actiontext (= 6.0.3.7)
actionview (= 6.0.3.7)
activejob (= 6.0.3.7)
activemodel (= 6.0.3.7)
activerecord (= 6.0.3.7)
activestorage (= 6.0.3.7)
activesupport (= 6.0.3.7)
bundler (>= 1.3.0)
railties (= 6.0.3.6)
railties (= 6.0.3.7)
sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
@ -989,9 +990,9 @@ GEM
rails-i18n (6.0.0)
i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 7)
railties (6.0.3.6)
actionpack (= 6.0.3.6)
activesupport (= 6.0.3.6)
railties (6.0.3.7)
actionpack (= 6.0.3.7)
activesupport (= 6.0.3.7)
method_source
rake (>= 0.8.7)
thor (>= 0.20.3, < 2.0)
@ -1008,7 +1009,6 @@ GEM
msgpack (>= 0.4.3)
optimist (>= 3.0.0)
rchardet (1.8.0)
rdoc (6.1.2)
re2 (1.2.0)
recaptcha (4.13.1)
json
@ -1485,6 +1485,7 @@ DEPENDENCIES
gitlab-markup (~> 1.7.1)
gitlab-net-dns (~> 0.9.1)
gitlab-omniauth-openid-connect (~> 0.4.0)
gitlab-rdoc (~> 6.3.2)
gitlab-sidekiq-fetcher (= 0.5.6)
gitlab-styles (~> 6.2.0)
gitlab_chronic_duration (~> 0.10.6.2)
@ -1544,7 +1545,7 @@ DEPENDENCIES
net-ldap (~> 0.16.3)
net-ntp
net-ssh (~> 6.0)
nokogiri (~> 1.11.1)
nokogiri (~> 1.11.4)
oauth2 (~> 1.4)
octokit (~> 4.15)
ohai (~> 16.10)
@ -1587,14 +1588,13 @@ DEPENDENCIES
rack-oauth2 (~> 1.16.0)
rack-proxy (~> 0.6.0)
rack-timeout (~> 0.5.1)
rails (~> 6.0.3.6)
rails (~> 6.0.3.7)
rails-controller-testing
rails-i18n (~> 6.0)
rainbow (~> 3.0)
raindrops (~> 0.18)
rblineprof (~> 0.3.6)
rbtrace (~> 0.4)
rdoc (~> 6.1.2)
re2 (~> 1.2.0)
recaptcha (~> 4.11)
redis (~> 4.0)

View file

@ -1 +1 @@
13.12.4
13.12.6

View file

@ -1,4 +1,5 @@
import $ from 'jquery';
import { sanitize } from '~/lib/dompurify';
import { getSelectedFragment, insertText } from '~/lib/utils/common_utils';
export class CopyAsGFM {
@ -69,7 +70,7 @@ export class CopyAsGFM {
} else {
// Due to the async copy call we are not able to produce gfm so we transform the cached HTML
const div = document.createElement('div');
div.innerHTML = gfmHtml;
div.innerHTML = sanitize(gfmHtml);
CopyAsGFM.nodeToGFM(div)
.then((transformedGfm) => {
CopyAsGFM.insertPastedText(e.target, text, transformedGfm);

View file

@ -535,3 +535,27 @@ export function getURLOrigin(url) {
return null;
}
}
/**
* Returns `true` if the given `url` resolves to the same origin the page is served
* from; otherwise, returns `false`.
*
* The `url` may be absolute or relative.
*
* @param {string} url The URL to check.
* @returns {boolean}
*/
export function isSameOriginUrl(url) {
if (typeof url !== 'string') {
return false;
}
const { origin } = window.location;
try {
return new URL(url, origin).origin === origin;
} catch {
// Invalid URLs cannot have the same origin
return false;
}
}

View file

@ -2,6 +2,7 @@
import { GlButton, GlFormInput, GlFormGroup, GlSprintf } from '@gitlab/ui';
import { mapState, mapActions, mapGetters } from 'vuex';
import { getParameterByName } from '~/lib/utils/common_utils';
import { isSameOriginUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import MilestoneCombobox from '~/milestones/components/milestone_combobox.vue';
import { BACK_URL_PARAM } from '~/releases/constants';
@ -65,7 +66,13 @@ export default {
},
},
cancelPath() {
return getParameterByName(BACK_URL_PARAM) || this.releasesPagePath;
const backUrl = getParameterByName(BACK_URL_PARAM);
if (isSameOriginUrl(backUrl)) {
return backUrl;
}
return this.releasesPagePath;
},
saveButtonLabel() {
return this.isExistingRelease ? __('Save changes') : __('Create release');

View file

@ -1,5 +1,5 @@
<template>
<div class="nothing-here-block">
{{ __('This diff was suppressed by a .gitattributes entry.') }}
{{ __("File suppressed by a .gitattributes entry or the file's encoding is unsupported.") }}
</div>
</template>

View file

@ -208,7 +208,10 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
params[:application_setting][:import_sources]&.delete("")
params[:application_setting][:restricted_visibility_levels]&.delete("")
params[:application_setting][:required_instance_ci_template] = nil if params[:application_setting][:required_instance_ci_template].blank?
if params[:application_setting].key?(:required_instance_ci_template)
params[:application_setting][:required_instance_ci_template] = nil if params[:application_setting][:required_instance_ci_template].empty?
end
remove_blank_params_for!(:elasticsearch_aws_secret_access_key, :eks_secret_access_key)
@ -217,9 +220,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
params.delete(:domain_denylist_raw) if params[:domain_denylist]
params.delete(:domain_allowlist_raw) if params[:domain_allowlist]
params.require(:application_setting).permit(
visible_application_setting_attributes
)
params[:application_setting].permit(visible_application_setting_attributes)
end
def recheck_user_consent?

View file

@ -108,7 +108,7 @@ module MembershipActions
respond_to do |format|
format.html do
redirect_path = member.request? ? member.source : [:dashboard, membershipable.class.to_s.tableize]
redirect_path = member.request? ? member.source : [:dashboard, membershipable.class.to_s.tableize.to_sym]
redirect_to redirect_path, notice: notice
end

View file

@ -20,12 +20,16 @@ class GraphqlController < ApplicationController
# around in GraphiQL.
protect_from_forgery with: :null_session, only: :execute
before_action :authorize_access_api!
# must come first: current_user is set up here
before_action(only: [:execute]) { authenticate_sessionless_user!(:api) }
before_action :authorize_access_api!
before_action :set_user_last_activity
before_action :track_vs_code_usage
before_action :disable_query_limiting
before_action :disallow_mutations_for_get
# Since we deactivate authentication from the main ApplicationController and
# defer it to :authorize_access_api!, we need to override the bypass session
# callback execution order here
@ -62,6 +66,25 @@ class GraphqlController < ApplicationController
private
def disallow_mutations_for_get
return unless request.get? || request.head?
return unless any_mutating_query?
raise ::Gitlab::Graphql::Errors::ArgumentError, "Mutations are forbidden in #{request.request_method} requests"
end
def any_mutating_query?
if multiplex?
multiplex_queries.any? { |q| mutation?(q[:query], q[:operation_name]) }
else
mutation?(query)
end
end
def mutation?(query_string, operation_name = params[:operationName])
::GraphQL::Query.new(GitlabSchema, query_string, operation_name: operation_name).mutation?
end
# Tests may mark some GraphQL queries as exempt from SQL query limits
def disable_query_limiting
return unless Gitlab::QueryLimiting.enabled_for_env?
@ -130,7 +153,9 @@ class GraphqlController < ApplicationController
end
def authorize_access_api!
access_denied!("API not accessible for user.") unless can?(current_user, :access_api)
return if can?(current_user, :access_api)
render_error('API not accessible for user', status: :forbidden)
end
# Overridden from the ApplicationController to make the response look like

View file

@ -7,6 +7,8 @@ class IdeController < ApplicationController
include StaticObjectExternalStorageCSP
include Gitlab::Utils::StrongMemoize
before_action :authorize_read_project!
before_action do
push_frontend_feature_flag(:build_service_proxy)
push_frontend_feature_flag(:schema_linting)
@ -22,6 +24,10 @@ class IdeController < ApplicationController
private
def authorize_read_project!
render_404 unless can?(current_user, :read_project, project)
end
def define_index_vars
return unless project

View file

@ -0,0 +1,33 @@
# frozen_string_literal: true
module Mutations
class Echo < BaseMutation
graphql_name 'EchoCreate'
description <<~DOC
A mutation that does not perform any changes.
This is expected to be used for testing of endpoints, to verify
that a user has mutation access.
DOC
argument :errors,
type: [::GraphQL::STRING_TYPE],
required: false,
description: 'Errors to return to the user.'
argument :messages,
type: [::GraphQL::STRING_TYPE],
as: :echoes,
required: false,
description: 'Messages to return to the user.'
field :echoes,
type: [::GraphQL::STRING_TYPE],
null: true,
description: 'Messages returned to the user.'
def resolve(**args)
args
end
end
end

View file

@ -101,6 +101,7 @@ module Types
mount_mutation Mutations::Ci::Job::Retry
mount_mutation Mutations::Namespace::PackageSettings::Update
mount_mutation Mutations::UserCallouts::Create
mount_mutation Mutations::Echo
end
end

View file

@ -32,6 +32,9 @@ class AuditEvent < ApplicationRecord
scope :by_author_id, -> (author_id) { where(author_id: author_id) }
after_initialize :initialize_details
before_validation :sanitize_message
# Note: The intention is to remove this once refactoring of AuditEvent
# has proceeded further.
#
@ -83,6 +86,14 @@ class AuditEvent < ApplicationRecord
private
def sanitize_message
message = details[:custom_message]
return unless message
self.details = details.merge(custom_message: Sanitize.clean(message))
end
def default_author_value
::Gitlab::Audit::NullAuthor.for(author_id, (self[:author_name] || details[:author_name]))
end

View file

@ -173,6 +173,7 @@ module Integrations
query_params[:os_authType] = 'basic'
params[:basic_auth] = basic_auth
params[:use_read_total_timeout] = true
params
end

View file

@ -50,9 +50,11 @@ class DroneCiService < CiService
end
def calculate_reactive_cache(sha, ref)
response = Gitlab::HTTP.try_get(commit_status_path(sha, ref),
response = Gitlab::HTTP.try_get(
commit_status_path(sha, ref),
verify: enable_ssl_verification,
extra_log_info: { project_id: project_id })
extra_log_info: { project_id: project_id },
use_read_total_timeout: true)
status =
if response && response.code == 200 && response['status']

View file

@ -38,7 +38,7 @@ class ExternalWikiService < Integration
end
def execute(_data)
response = Gitlab::HTTP.get(properties['external_wiki_url'], verify: true)
response = Gitlab::HTTP.get(properties['external_wiki_url'], verify: true, use_read_total_timeout: true)
response.body if response.code == 200
rescue StandardError
nil

View file

@ -106,7 +106,7 @@ class IssueTrackerService < Integration
result = false
begin
response = Gitlab::HTTP.head(self.project_url, verify: true)
response = Gitlab::HTTP.head(self.project_url, verify: true, use_read_total_timeout: true)
if response
message = "#{self.type} received response #{response.code} when attempting to connect to #{self.project_url}"

View file

@ -56,7 +56,7 @@ class MockCiService < CiService
#
#
def commit_status(sha, ref)
response = Gitlab::HTTP.get(commit_status_path(sha), verify: false)
response = Gitlab::HTTP.get(commit_status_path(sha), verify: false, use_read_total_timeout: true)
read_commit_status(response)
rescue Errno::ECONNREFUSED
:error

View file

@ -17,7 +17,7 @@ module SlackMattermost
class HTTPClient
def self.post(uri, params = {})
params.delete(:http_options) # these are internal to the client and we do not want them
Gitlab::HTTP.post(uri, body: params)
Gitlab::HTTP.post(uri, body: params, use_read_total_timeout: true)
end
end
end

View file

@ -169,7 +169,7 @@ class TeamcityService < CiService
end
def get_path(path)
Gitlab::HTTP.try_get(build_url(path), verify: false, basic_auth: basic_auth, extra_log_info: { project_id: project_id })
Gitlab::HTTP.try_get(build_url(path), verify: false, basic_auth: basic_auth, extra_log_info: { project_id: project_id }, use_read_total_timeout: true)
end
def post_to_build_queue(data, branch)
@ -179,7 +179,8 @@ class TeamcityService < CiService
"<buildType id=#{build_type.encode(xml: :attr)}/>"\
'</build>',
headers: { 'Content-type' => 'application/xml' },
basic_auth: basic_auth
basic_auth: basic_auth,
use_read_total_timeout: true
)
end

View file

@ -48,7 +48,8 @@ class UnifyCircuitService < ChatNotificationService
response = Gitlab::HTTP.post(webhook, body: {
subject: message.project_name,
text: message.summary,
markdown: true
markdown: true,
use_read_total_timeout: true
}.to_json)
response if response.success?

View file

@ -43,7 +43,7 @@ class WebexTeamsService < ChatNotificationService
def notify(message, opts)
header = { 'Content-Type' => 'application/json' }
response = Gitlab::HTTP.post(webhook, headers: header, body: { markdown: message.summary }.to_json)
response = Gitlab::HTTP.post(webhook, headers: header, body: { markdown: message.summary }.to_json, use_read_total_timeout: true)
response if response.success?
end

View file

@ -20,7 +20,7 @@ class ProtectedBranch::PushAccessLevel < ApplicationRecord
def check_access(user)
if user && deploy_key.present?
return true if user.can?(:read_project, project) && enabled_deploy_key_for_user?(deploy_key, user)
return user.can?(:read_project, project) && enabled_deploy_key_for_user?(deploy_key, user)
end
super

View file

@ -238,6 +238,7 @@ class User < ApplicationRecord
validate :owns_commit_email, if: :commit_email_changed?
validate :signup_domain_valid?, on: :create, if: ->(user) { !user.created_by_id }
validate :check_email_restrictions, on: :create, if: ->(user) { !user.created_by_id }
validate :check_username_format, if: :username_changed?
validates :theme_id, allow_nil: true, inclusion: { in: Gitlab::Themes.valid_ids,
message: _("%{placeholder} is not a valid theme") % { placeholder: '%{value}' } }
@ -1255,12 +1256,23 @@ class User < ApplicationRecord
end
def sanitize_attrs
sanitize_links
sanitize_name
end
def sanitize_links
%i[skype linkedin twitter].each do |attr|
value = self[attr]
self[attr] = Sanitize.clean(value) if value.present?
end
end
def sanitize_name
return unless self.name
self.name = self.name.gsub(%r{</?[^>]*>}, '')
end
def set_notification_email
if notification_email.blank? || all_emails.exclude?(notification_email)
self.notification_email = email
@ -1873,6 +1885,12 @@ class User < ApplicationRecord
!!(password_expires_at && password_expires_at < Time.current)
end
def password_expired_if_applicable?
return false unless allow_password_authentication?
password_expired?
end
def can_be_deactivated?
active? && no_recent_activity? && !internal?
end
@ -2066,6 +2084,12 @@ class User < ApplicationRecord
end
end
def check_username_format
return if username.blank? || Mime::EXTENSION_LOOKUP.keys.none? { |type| username.end_with?(type) }
errors.add(:username, _('ending with MIME type format is not allowed.'))
end
def groups_with_developer_maintainer_project_access
project_creation_levels = [::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS]

View file

@ -81,7 +81,7 @@ module PolicyActor
false
end
def password_expired?
def password_expired_if_applicable?
false
end
end

View file

@ -16,7 +16,7 @@ class GlobalPolicy < BasePolicy
end
condition(:password_expired, scope: :user) do
@user&.password_expired?
@user&.password_expired_if_applicable?
end
condition(:project_bot, scope: :user) { @user&.project_bot? }

View file

@ -49,9 +49,9 @@ module FeatureFlags
end
def created_scope_message(scope)
"Created rule <strong>#{scope.environment_scope}</strong> "\
"and set it as <strong>#{scope.active ? "active" : "inactive"}</strong> "\
"with strategies <strong>#{scope.strategies}</strong>."
"Created rule #{scope.environment_scope} "\
"and set it as #{scope.active ? "active" : "inactive"} "\
"with strategies #{scope.strategies}."
end
def feature_flag_by_name

View file

@ -22,8 +22,7 @@ module FeatureFlags
private
def audit_message(feature_flag)
message_parts = ["Created feature flag <strong>#{feature_flag.name}</strong>",
"with description <strong>\"#{feature_flag.description}\"</strong>."]
message_parts = ["Created feature flag #{feature_flag.name} with description \"#{feature_flag.description}\"."]
message_parts += feature_flag.scopes.map do |scope|
created_scope_message(scope)

View file

@ -23,7 +23,7 @@ module FeatureFlags
end
def audit_message(feature_flag)
"Deleted feature flag <strong>#{feature_flag.name}</strong>."
"Deleted feature flag #{feature_flag.name}."
end
def can_destroy?(feature_flag)

View file

@ -45,14 +45,14 @@ module FeatureFlags
return if changes.empty?
"Updated feature flag <strong>#{feature_flag.name}</strong>. " + changes.join(" ")
"Updated feature flag #{feature_flag.name}. " + changes.join(" ")
end
def changed_attributes_messages(feature_flag)
feature_flag.changes.slice(*AUDITABLE_ATTRIBUTES).map do |attribute_name, changes|
"Updated #{attribute_name} "\
"from <strong>\"#{changes.first}\"</strong> to "\
"<strong>\"#{changes.second}\"</strong>."
"from \"#{changes.first}\" to "\
"\"#{changes.second}\"."
end
end
@ -69,17 +69,17 @@ module FeatureFlags
end
def deleted_scope_message(scope)
"Deleted rule <strong>#{scope.environment_scope}</strong>."
"Deleted rule #{scope.environment_scope}."
end
def updated_scope_message(scope)
changes = scope.changes.slice(*AUDITABLE_SCOPE_ATTRIBUTES_HUMAN_NAMES.keys)
return if changes.empty?
message = "Updated rule <strong>#{scope.environment_scope}</strong> "
message = "Updated rule #{scope.environment_scope} "
message += changes.map do |attribute_name, change|
name = AUDITABLE_SCOPE_ATTRIBUTES_HUMAN_NAMES[attribute_name]
"#{name} from <strong>#{change.first}</strong> to <strong>#{change.second}</strong>"
"#{name} from #{change.first} to #{change.second}"
end.join(' ')
message + '.'

View file

@ -34,8 +34,9 @@ module Projects
new_project = CreateService.new(current_user, new_fork_params).execute
return new_project unless new_project.persisted?
builds_access_level = @project.project_feature.builds_access_level
new_project.project_feature.update(builds_access_level: builds_access_level)
new_project.project_feature.update!(
@project.project_feature.slice(ProjectFeature::FEATURES.map { |f| "#{f}_access_level" })
)
new_project
end

View file

@ -41,6 +41,7 @@ class WebHookService
@hook_name = hook_name.to_s
@request_options = {
timeout: Gitlab.config.gitlab.webhook_timeout,
use_read_total_timeout: true,
allow_local_requests: hook.allow_local_requests?
}
end
@ -67,7 +68,7 @@ class WebHookService
{
status: :success,
http_status: response.code,
message: response.to_s
message: response.body
}
rescue SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::EHOSTUNREACH,
Net::OpenTimeout, Net::ReadTimeout, Gitlab::HTTP::BlockedUrlError, Gitlab::HTTP::RedirectionTooDeep,

View file

@ -1,5 +1,5 @@
- add_page_specific_style 'page_bundles/import'
- provider = local_assigns.fetch(:provider)
- provider = local_assigns.fetch(:provider).to_sym
- extra_data = local_assigns.fetch(:extra_data, {})
- filterable = local_assigns.fetch(:filterable, true)
- paginatable = local_assigns.fetch(:paginatable, false)

View file

@ -1,2 +1,2 @@
.nothing-here-block
= _("This diff was suppressed by a .gitattributes entry.")
= _("File suppressed by a .gitattributes entry or the file's encoding is unsupported.")

View file

@ -3,7 +3,7 @@
- labels = issuable.labels
- assignees = issuable.assignees
- base_url_args = [project]
- issuable_type_args = base_url_args + [issuable.class.table_name]
- issuable_type_args = base_url_args + [issuable.class.table_name.to_sym]
- issuable_url_args = base_url_args + [issuable]
%li.issuable-row

View file

@ -1959,6 +1959,31 @@ Input type: `DismissVulnerabilityInput`
| <a id="mutationdismissvulnerabilityerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationdismissvulnerabilityvulnerability"></a>`vulnerability` | [`Vulnerability`](#vulnerability) | The vulnerability after dismissal. |
### `Mutation.echoCreate`
A mutation that does not perform any changes.
This is expected to be used for testing of endpoints, to verify
that a user has mutation access.
Input type: `EchoCreateInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationechocreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationechocreateerrors"></a>`errors` | [`[String!]`](#string) | Errors to return to the user. |
| <a id="mutationechocreatemessages"></a>`messages` | [`[String!]`](#string) | Messages to return to the user. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationechocreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationechocreateechoes"></a>`echoes` | [`[String!]`](#string) | Messages returned to the user. |
| <a id="mutationechocreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
### `Mutation.environmentsCanaryIngressUpdate`
Input type: `EnvironmentsCanaryIngressUpdateInput`

View file

@ -96,6 +96,8 @@ module API
end
# rubocop: disable CodeReuse/ActiveRecord
post ":id/members" do
::Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/333434')
source = find_source(source_type, params[:id])
authorize_admin_source!(source_type, source)

View file

@ -382,7 +382,7 @@ module Gitlab
end
def can_user_login_with_non_expired_password?(user)
user.can?(:log_in) && !user.password_expired?
user.can?(:log_in) && !user.password_expired_if_applicable?
end
end
end

View file

@ -23,6 +23,8 @@ module Gitlab
"Your primary email address is not confirmed. "\
"Please check your inbox for the confirmation instructions. "\
"In case the link is expired, you can request a new confirmation email at #{Rails.application.routes.url_helpers.new_user_confirmation_url}"
when :blocked
"Your account has been blocked."
when :password_expired
"Your password expired. "\
"Please access GitLab from a web browser to update your password."
@ -44,6 +46,8 @@ module Gitlab
:deactivated
elsif !@user.confirmed?
:unconfirmed
elsif @user.blocked?
:blocked
elsif @user.password_expired?
:password_expired
else

View file

@ -250,7 +250,7 @@ module Gitlab
end
def diffable?
repository.attributes(file_path).fetch('diff') { true }
diffable_by_attribute? && !text_with_binary_notice?
end
def binary_in_repo?
@ -366,6 +366,15 @@ module Gitlab
private
def diffable_by_attribute?
repository.attributes(file_path).fetch('diff') { true }
end
# NOTE: Files with unsupported encodings (e.g. UTF-16) are treated as binary by git, but they are recognized as text files during encoding detection. These files have `Binary files a/filename and b/filename differ' as their raw diff content which cannot be used. We need to handle this special case and avoid displaying incorrect diff.
def text_with_binary_notice?
text? && has_binary_notice?
end
def fetch_blob(sha, path)
return unless sha

View file

@ -6,7 +6,7 @@ module Gitlab
include Enumerable
def parse(lines, diff_file: nil)
return [] if lines.blank?
return [] if lines.blank? || Git::Diff.has_binary_notice?(lines.first)
@lines = lines
line_obj_index = 0

View file

@ -33,6 +33,8 @@ module Gitlab
SERIALIZE_KEYS = %i(diff new_path old_path a_mode b_mode new_file renamed_file deleted_file too_large).freeze
BINARY_NOTICE_PATTERN = %r(Binary files a\/(.*) and b\/(.*) differ).freeze
class << self
def between(repo, head, base, options = {}, *paths)
straight = options.delete(:straight) || false
@ -131,8 +133,13 @@ module Gitlab
def patch_hard_limit_bytes
Gitlab::CurrentSettings.diff_max_patch_bytes
end
end
def has_binary_notice?(text)
return false unless text.present?
text.start_with?(BINARY_NOTICE_PATTERN)
end
end
def initialize(raw_diff, expanded: true)
@expanded = expanded
@ -215,7 +222,7 @@ module Gitlab
end
def has_binary_notice?
@diff.start_with?('Binary')
self.class.has_binary_notice?(@diff)
end
private

View file

@ -8,9 +8,10 @@ module Gitlab
class HTTP
BlockedUrlError = Class.new(StandardError)
RedirectionTooDeep = Class.new(StandardError)
ReadTotalTimeout = Class.new(Net::ReadTimeout)
HTTP_TIMEOUT_ERRORS = [
Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout
Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout, Gitlab::HTTP::ReadTotalTimeout
].freeze
HTTP_ERRORS = HTTP_TIMEOUT_ERRORS + [
SocketError, OpenSSL::SSL::SSLError, OpenSSL::OpenSSLError,
@ -23,6 +24,7 @@ module Gitlab
read_timeout: 20,
write_timeout: 30
}.freeze
DEFAULT_READ_TOTAL_TIMEOUT = 20.seconds
include HTTParty # rubocop:disable Gitlab/HTTParty
@ -41,7 +43,19 @@ module Gitlab
options
end
httparty_perform_request(http_method, path, options_with_timeouts, &block)
unless options.has_key?(:use_read_total_timeout)
return httparty_perform_request(http_method, path, options_with_timeouts, &block)
end
start_time = Gitlab::Metrics::System.monotonic_time
read_total_timeout = options.fetch(:timeout, DEFAULT_READ_TOTAL_TIMEOUT)
httparty_perform_request(http_method, path, options_with_timeouts) do |fragment|
elapsed = Gitlab::Metrics::System.monotonic_time - start_time
raise ReadTotalTimeout, "Request timed out after #{elapsed} seconds" if elapsed > read_total_timeout
block.call fragment if block
end
rescue HTTParty::RedirectionTooDeep
raise RedirectionTooDeep
rescue *HTTP_ERRORS => e

View file

@ -52,7 +52,7 @@ module Gitlab
def valid_user?
return true unless user?
!actor.blocked? && (!actor.allow_password_authentication? || !actor.password_expired?)
!actor.blocked? && !actor.password_expired_if_applicable?
end
def authentication_payload(repository_http_path)

View file

@ -14015,6 +14015,9 @@ msgstr ""
msgid "File renamed with no changes."
msgstr ""
msgid "File suppressed by a .gitattributes entry or the file's encoding is unsupported."
msgstr ""
msgid "File synchronization concurrency limit"
msgstr ""
@ -33097,9 +33100,6 @@ msgstr ""
msgid "This diff is collapsed."
msgstr ""
msgid "This diff was suppressed by a .gitattributes entry."
msgstr ""
msgid "This directory"
msgstr ""
@ -38357,6 +38357,9 @@ msgstr ""
msgid "encrypted: needs to be a :required, :optional or :migrating!"
msgstr ""
msgid "ending with MIME type format is not allowed."
msgstr ""
msgid "entries cannot be larger than 255 characters"
msgstr ""

View file

@ -44,7 +44,7 @@ RSpec.describe GraphqlController do
expect(response).to have_gitlab_http_status(:ok)
end
it 'returns access denied template when user cannot access API' do
it 'returns forbidden when user cannot access API' do
# User cannot access API in a couple of cases
# * When user is internal(like ghost users)
# * When user is blocked
@ -54,7 +54,9 @@ RSpec.describe GraphqlController do
post :execute
expect(response).to have_gitlab_http_status(:forbidden)
expect(response).to render_template('errors/access_denied')
expect(json_response).to include(
'errors' => include(a_hash_including('message' => /API not accessible/))
)
end
it 'updates the users last_activity_on field' do

View file

@ -55,9 +55,9 @@ module DeprecationToolkitEnv
# one by one
def self.allowed_kwarg_warning_paths
%w[
activerecord-6.0.3.6/lib/active_record/migration.rb
activesupport-6.0.3.6/lib/active_support/cache.rb
activerecord-6.0.3.6/lib/active_record/relation.rb
activerecord-6.0.3.7/lib/active_record/migration.rb
activesupport-6.0.3.7/lib/active_support/cache.rb
activerecord-6.0.3.7/lib/active_record/relation.rb
asciidoctor-2.0.12/lib/asciidoctor/extensions.rb
attr_encrypted-3.1.0/lib/attr_encrypted/adapters/active_record.rb
]

View file

@ -253,7 +253,7 @@ RSpec.describe 'Expand and collapse diffs', :js do
click_link('Expand all')
# Wait for elements to appear to ensure full page reload
expect(page).to have_content('This diff was suppressed by a .gitattributes entry')
expect(page).to have_content("File suppressed by a .gitattributes entry or the file's encoding is unsupported.")
expect(page).to have_content('This source diff could not be displayed because it is too large.')
expect(page).to have_content('too_large_image.jpg')
find('.note-textarea')

View file

@ -174,4 +174,14 @@ RSpec.describe 'Diff file viewer', :js, :with_clean_rails_cache do
end
end
end
context 'when the the encoding of the file is unsupported' do
before do
visit_commit('f05a98786e4274708e1fa118c7ad3a29d1d1b9a3')
end
it 'shows it is not diffable' do
expect(page).to have_content("File suppressed by a .gitattributes entry or the file's encoding is unsupported.")
end
end
end

View file

@ -65,18 +65,6 @@ RSpec.describe 'Comments on personal snippets', :js do
expect(page).to have_content(user_name)
end
end
context 'when the author name contains HTML' do
let(:user_name) { '<h1><a href="https://bad.link/malicious.exe" class="evil">Fake Content<img class="fake-icon" src="image.png"></a></h1>' }
it 'renders the name as plain text' do
visit snippet_path(snippet)
content = find("#note_#{snippet_notes[0].id} .note-header-author-name").text
expect(content).to eq user_name
end
end
end
context 'when submitting a note' do

View file

@ -1,50 +1,54 @@
import initCopyAsGFM, { CopyAsGFM } from '~/behaviors/markdown/copy_as_gfm';
import * as commonUtils from '~/lib/utils/common_utils';
describe('CopyAsGFM', () => {
describe('CopyAsGFM.pasteGFM', () => {
function callPasteGFM() {
let target;
beforeEach(() => {
target = document.createElement('input');
target.value = 'This is code: ';
});
// When GFM code is copied, we put the regular plain text
// on the clipboard as `text/plain`, and the GFM as `text/x-gfm`.
// This emulates the behavior of `getData` with that data.
function callPasteGFM(data = { 'text/plain': 'code', 'text/x-gfm': '`code`' }) {
const e = {
originalEvent: {
clipboardData: {
getData(mimeType) {
// When GFM code is copied, we put the regular plain text
// on the clipboard as `text/plain`, and the GFM as `text/x-gfm`.
// This emulates the behavior of `getData` with that data.
if (mimeType === 'text/plain') {
return 'code';
}
if (mimeType === 'text/x-gfm') {
return '`code`';
}
return null;
return data[mimeType] || null;
},
},
},
preventDefault() {},
target,
};
CopyAsGFM.pasteGFM(e);
}
it('wraps pasted code when not already in code tags', () => {
jest.spyOn(commonUtils, 'insertText').mockImplementation((el, textFunc) => {
const insertedText = textFunc('This is code: ', '');
expect(insertedText).toEqual('`code`');
});
callPasteGFM();
expect(target.value).toBe('This is code: `code`');
});
it('does not wrap pasted code when already in code tags', () => {
jest.spyOn(commonUtils, 'insertText').mockImplementation((el, textFunc) => {
const insertedText = textFunc('This is code: `', '`');
expect(insertedText).toEqual('code');
});
target.value = 'This is code: `';
callPasteGFM();
expect(target.value).toBe('This is code: `code');
});
it('does not allow xss in x-gfm-html', () => {
const testEl = document.createElement('div');
jest.spyOn(document, 'createElement').mockReturnValueOnce(testEl);
callPasteGFM({ 'text/plain': 'code', 'text/x-gfm-html': 'code<img/src/onerror=alert(1)>' });
expect(testEl.innerHTML).toBe('code<img src="">');
});
});

View file

@ -1,3 +1,4 @@
import { TEST_HOST } from 'helpers/test_constants';
import * as urlUtils from '~/lib/utils/url_utility';
const shas = {
@ -921,4 +922,37 @@ describe('URL utility', () => {
expect(urlUtils.encodeSaferUrl(input)).toBe(input);
});
});
describe('isSameOriginUrl', () => {
// eslint-disable-next-line no-script-url
const javascriptUrl = 'javascript:alert(1)';
beforeEach(() => {
setWindowLocation({ origin: TEST_HOST });
});
it.each`
url | expected
${TEST_HOST} | ${true}
${`${TEST_HOST}/a/path`} | ${true}
${'//test.host/no-protocol'} | ${true}
${'/a/root/relative/path'} | ${true}
${'a/relative/path'} | ${true}
${'#hash'} | ${true}
${'?param=foo'} | ${true}
${''} | ${true}
${'../../../'} | ${true}
${`${TEST_HOST}:8080/wrong-port`} | ${false}
${'ws://test.host/wrong-protocol'} | ${false}
${'http://phishing.test'} | ${false}
${'//phishing.test'} | ${false}
${'//invalid:url'} | ${false}
${javascriptUrl} | ${false}
${'data:,Hello%2C%20World%21'} | ${false}
${null} | ${false}
${undefined} | ${false}
`('returns $expected given $url', ({ url, expected }) => {
expect(urlUtils.isSameOriginUrl(url)).toBe(expected);
});
});
});

View file

@ -4,6 +4,7 @@ import MockAdapter from 'axios-mock-adapter';
import { merge } from 'lodash';
import Vuex from 'vuex';
import { getJSONFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
import * as commonUtils from '~/lib/utils/common_utils';
import ReleaseEditNewApp from '~/releases/components/app_edit_new.vue';
import AssetLinksForm from '~/releases/components/asset_links_form.vue';
@ -11,6 +12,7 @@ import { BACK_URL_PARAM } from '~/releases/constants';
const originalRelease = getJSONFixture('api/releases/release.json');
const originalMilestones = originalRelease.milestones;
const releasesPagePath = 'path/to/releases/page';
describe('Release edit/new component', () => {
let wrapper;
@ -24,7 +26,7 @@ describe('Release edit/new component', () => {
state = {
release,
markdownDocsPath: 'path/to/markdown/docs',
releasesPagePath: 'path/to/releases/page',
releasesPagePath,
projectId: '8',
groupId: '42',
groupMilestonesAvailable: true,
@ -75,6 +77,8 @@ describe('Release edit/new component', () => {
};
beforeEach(() => {
global.jsdom.reconfigure({ url: TEST_HOST });
mock = new MockAdapter(axios);
gon.api_version = 'v4';
@ -146,22 +150,33 @@ describe('Release edit/new component', () => {
});
});
describe(`when the URL contains a "${BACK_URL_PARAM}" parameter`, () => {
const backUrl = 'https://example.gitlab.com/back/url';
// eslint-disable-next-line no-script-url
const xssBackUrl = 'javascript:alert(1)';
describe.each`
backUrl | expectedHref
${`${TEST_HOST}/back/url`} | ${`${TEST_HOST}/back/url`}
${`/back/url?page=2`} | ${`/back/url?page=2`}
${`back/url?page=3`} | ${`back/url?page=3`}
${'http://phishing.test/back/url'} | ${releasesPagePath}
${'//phishing.test/back/url'} | ${releasesPagePath}
${xssBackUrl} | ${releasesPagePath}
`(
`when the URL contains a "${BACK_URL_PARAM}=$backUrl" parameter`,
({ backUrl, expectedHref }) => {
beforeEach(async () => {
global.jsdom.reconfigure({
url: `${TEST_HOST}?${BACK_URL_PARAM}=${encodeURIComponent(backUrl)}`,
});
beforeEach(async () => {
commonUtils.getParameterByName = jest
.fn()
.mockImplementation((paramToGet) => ({ [BACK_URL_PARAM]: backUrl }[paramToGet]));
await factory();
});
await factory();
});
it('renders a "Cancel" button with an href pointing to the main Releases page', () => {
const cancelButton = wrapper.find('.js-cancel-button');
expect(cancelButton.attributes().href).toBe(backUrl);
});
});
it(`renders a "Cancel" button with an href pointing to ${expectedHref}`, () => {
const cancelButton = wrapper.find('.js-cancel-button');
expect(cancelButton.attributes().href).toBe(expectedHref);
});
},
);
describe('when creating a new release', () => {
beforeEach(async () => {

View file

@ -23,7 +23,7 @@ RSpec.describe Gitlab::Diff::FileCollection::MergeRequestDiff do
it 'does not highlight files marked as undiffable in .gitattributes' do
allow_next_instance_of(Gitlab::Diff::File) do |instance|
allow(instance).to receive(:diffable?).and_return(false)
allow(instance).to receive(:diffable_by_attribute?).and_return(false)
end
expect_next_instance_of(Gitlab::Diff::File) do |instance|

View file

@ -186,26 +186,46 @@ RSpec.describe Gitlab::Diff::File do
end
describe '#diffable?' do
let(:commit) { project.commit('1a0b36b3cdad1d2ee32457c102a8c0b7056fa863') }
let(:diffs) { commit.diffs }
context 'when attributes exist' do
let(:commit) { project.commit('1a0b36b3cdad1d2ee32457c102a8c0b7056fa863') }
let(:diffs) { commit.diffs }
before do
info_dir_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
File.join(project.repository.path_to_repo, 'info')
before do
info_dir_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
File.join(project.repository.path_to_repo, 'info')
end
FileUtils.mkdir(info_dir_path) unless File.exist?(info_dir_path)
File.write(File.join(info_dir_path, 'attributes'), "*.md -diff\n")
end
FileUtils.mkdir(info_dir_path) unless File.exist?(info_dir_path)
File.write(File.join(info_dir_path, 'attributes'), "*.md -diff\n")
it "returns true for files that do not have attributes" do
diff_file = diffs.diff_file_with_new_path('LICENSE')
expect(diff_file.diffable?).to be_truthy
end
it "returns false for files that have been marked as not being diffable in attributes" do
diff_file = diffs.diff_file_with_new_path('README.md')
expect(diff_file.diffable?).to be_falsey
end
end
it "returns true for files that do not have attributes" do
diff_file = diffs.diff_file_with_new_path('LICENSE')
expect(diff_file.diffable?).to be_truthy
context 'when the text has binary notice' do
let(:commit) { project.commit('f05a98786e4274708e1fa118c7ad3a29d1d1b9a3') }
let(:diff_file) { commit.diffs.diff_file_with_new_path('VERSION') }
it "returns false" do
expect(diff_file.diffable?).to be_falsey
end
end
it "returns false for files that have been marked as not being diffable in attributes" do
diff_file = diffs.diff_file_with_new_path('README.md')
expect(diff_file.diffable?).to be_falsey
context 'when the content is binary' do
let(:commit) { project.commit('2f63565e7aac07bcdadb654e253078b727143ec4') }
let(:diff_file) { commit.diffs.diff_file_with_new_path('files/images/6049019_460s.jpg') }
it "returns true" do
expect(diff_file.diffable?).to be_truthy
end
end
end
@ -729,6 +749,18 @@ RSpec.describe Gitlab::Diff::File do
end
end
context 'when the the encoding of the file is unsupported' do
let(:commit) { project.commit('f05a98786e4274708e1fa118c7ad3a29d1d1b9a3') }
let(:diff_file) { commit.diffs.diff_file_with_new_path('VERSION') }
it 'returns a Not Diffable viewer' do
expect(diff_file.simple_viewer).to be_a(DiffViewer::NotDiffable)
end
it { expect(diff_file.highlighted_diff_lines).to eq([]) }
it { expect(diff_file.parallel_diff_lines).to eq([]) }
end
describe '#diff_hunk' do
context 'when first line is a match' do
let(:raw_diff) do

View file

@ -146,6 +146,16 @@ eos
it { expect(parser.parse(nil)).to eq([]) }
end
context 'when it is a binary notice' do
let(:diff) do
<<~END
Binary files a/test and b/test differ
END
end
it { expect(parser.parse(diff.each_line)).to eq([]) }
end
describe 'tolerates special diff markers in a content' do
it "counts lines correctly" do
diff = <<~END

View file

@ -440,6 +440,14 @@ RSpec.describe Gitlab::GitAccess do
expect { pull_access_check }.to raise_forbidden("Your password expired. Please access GitLab from a web browser to update your password.")
end
it 'allows ldap users with expired password to pull' do
project.add_maintainer(user)
user.update!(password_expires_at: 2.minutes.ago)
allow(user).to receive(:ldap_user?).and_return(true)
expect { pull_access_check }.not_to raise_error
end
context 'when the project repository does not exist' do
before do
project.add_guest(user)
@ -977,12 +985,26 @@ RSpec.describe Gitlab::GitAccess do
end
it 'disallows users with expired password to push' do
project.add_maintainer(user)
user.update!(password_expires_at: 2.minutes.ago)
expect { push_access_check }.to raise_forbidden("Your password expired. Please access GitLab from a web browser to update your password.")
end
it 'allows ldap users with expired password to push' do
user.update!(password_expires_at: 2.minutes.ago)
allow(user).to receive(:ldap_user?).and_return(true)
expect { push_access_check }.not_to raise_error
end
it 'disallows blocked ldap users with expired password to push' do
user.block
user.update!(password_expires_at: 2.minutes.ago)
allow(user).to receive(:ldap_user?).and_return(true)
expect { push_access_check }.to raise_forbidden("Your account has been blocked.")
end
it 'cleans up the files' do
expect(project.repository).to receive(:clean_stale_repository_files).and_call_original
expect { push_access_check }.not_to raise_error

View file

@ -27,6 +27,47 @@ RSpec.describe Gitlab::HTTP do
end
end
context 'when reading the response is too slow' do
before do
stub_const("#{described_class}::DEFAULT_READ_TOTAL_TIMEOUT", 0.001.seconds)
WebMock.stub_request(:post, /.*/).to_return do |request|
sleep 0.002.seconds
{ body: 'I\m slow', status: 200 }
end
end
let(:options) { {} }
subject(:request_slow_responder) { described_class.post('http://example.org', **options) }
specify do
expect { request_slow_responder }.not_to raise_error
end
context 'with use_read_total_timeout option' do
let(:options) { { use_read_total_timeout: true } }
it 'raises a timeout error' do
expect { request_slow_responder }.to raise_error(Gitlab::HTTP::ReadTotalTimeout, /Request timed out after ?([0-9]*[.])?[0-9]+ seconds/)
end
context 'and timeout option' do
let(:options) { { use_read_total_timeout: true, timeout: 10.seconds } }
it 'overrides the default timeout when timeout option is present' do
expect { request_slow_responder }.not_to raise_error
end
end
end
end
it 'calls a block' do
WebMock.stub_request(:post, /.*/)
expect { |b| described_class.post('http://example.org', &b) }.to yield_with_args
end
describe 'allow_local_requests_from_web_hooks_and_services is' do
before do
WebMock.stub_request(:get, /.*/).to_return(status: 200, body: 'Success')

View file

@ -3,9 +3,6 @@
require 'spec_helper'
RSpec.describe AuditEvent do
let_it_be(:audit_event) { create(:project_audit_event) }
subject { audit_event }
describe 'validations' do
include_examples 'validates IP address' do
let(:attribute) { :ip_address }
@ -13,6 +10,15 @@ RSpec.describe AuditEvent do
end
end
it 'sanitizes custom_message in the details hash' do
audit_event = create(:project_audit_event, details: { target_id: 678, custom_message: '<strong>Arnold</strong>' })
expect(audit_event.details).to include(
target_id: 678,
custom_message: 'Arnold'
)
end
describe '#as_json' do
context 'ip_address' do
subject { build(:group_audit_event, ip_address: '192.168.1.1').as_json }

View file

@ -44,7 +44,7 @@ RSpec.describe ProtectedBranch::PushAccessLevel do
let(:can_push) { true }
before_all do
project.add_guest(user)
project.add_maintainer(user)
end
context 'when this push_access_level is tied to a deploy key' do

View file

@ -376,6 +376,19 @@ RSpec.describe User do
expect(user.errors.full_messages).to eq(['Username has already been taken'])
end
end
it 'validates format' do
Mime::EXTENSION_LOOKUP.keys.each do |type|
user = build(:user, username: "test.#{type}")
expect(user).not_to be_valid
expect(user.errors.full_messages).to include('Username ending with MIME type format is not allowed.')
end
end
it 'validates format on updated record' do
expect(create(:user).update(username: 'profile.html')).to be_falsey
end
end
it 'has a DB-level NOT NULL constraint on projects_limit' do
@ -2877,7 +2890,7 @@ RSpec.describe User do
end
describe '#sanitize_attrs' do
let(:user) { build(:user, name: 'test & user', skype: 'test&user') }
let(:user) { build(:user, name: 'test <& user', skype: 'test&user') }
it 'encodes HTML entities in the Skype attribute' do
expect { user.sanitize_attrs }.to change { user.skype }.to('test&amp;user')
@ -2886,6 +2899,25 @@ RSpec.describe User do
it 'does not encode HTML entities in the name attribute' do
expect { user.sanitize_attrs }.not_to change { user.name }
end
it 'sanitizes attr from html tags' do
user = create(:user, name: '<a href="//example.com">Test<a>', twitter: '<a href="//evil.com">https://twitter.com<a>')
expect(user.name).to eq('Test')
expect(user.twitter).to eq('https://twitter.com')
end
it 'sanitizes attr from js scripts' do
user = create(:user, name: '<script>alert("Test")</script>')
expect(user.name).to eq("alert(\"Test\")")
end
it 'sanitizes attr from iframe scripts' do
user = create(:user, name: 'User"><iframe src=javascript:alert()><iframe>')
expect(user.name).to eq('User">')
end
end
describe '#starred?' do
@ -5248,6 +5280,70 @@ RSpec.describe User do
end
end
describe '#password_expired_if_applicable?' do
let(:user) { build(:user, password_expires_at: password_expires_at) }
subject { user.password_expired_if_applicable? }
context 'when user is not ldap user' do
context 'when password_expires_at is not set' do
let(:password_expires_at) {}
it 'returns false' do
is_expected.to be_falsey
end
end
context 'when password_expires_at is in the past' do
let(:password_expires_at) { 1.minute.ago }
it 'returns true' do
is_expected.to be_truthy
end
end
context 'when password_expires_at is in the future' do
let(:password_expires_at) { 1.minute.from_now }
it 'returns false' do
is_expected.to be_falsey
end
end
end
context 'when user is ldap user' do
let(:user) { build(:user, password_expires_at: password_expires_at) }
before do
allow(user).to receive(:ldap_user?).and_return(true)
end
context 'when password_expires_at is not set' do
let(:password_expires_at) {}
it 'returns false' do
is_expected.to be_falsey
end
end
context 'when password_expires_at is in the past' do
let(:password_expires_at) { 1.minute.ago }
it 'returns false' do
is_expected.to be_falsey
end
end
context 'when password_expires_at is in the future' do
let(:password_expires_at) { 1.minute.from_now }
it 'returns false' do
is_expected.to be_falsey
end
end
end
end
describe '#read_only_attribute?' do
context 'when synced attributes metadata is present' do
it 'delegates to synced_attributes_metadata' do

View file

@ -239,12 +239,28 @@ RSpec.describe GlobalPolicy do
it { is_expected.not_to be_allowed(:access_api) }
end
context 'with a deactivated user' do
before do
current_user.deactivate!
end
it { is_expected.not_to be_allowed(:access_api) }
end
context 'user with expired password' do
before do
current_user.update!(password_expires_at: 2.minutes.ago)
end
it { is_expected.not_to be_allowed(:access_api) }
context 'when user is using ldap' do
before do
allow(current_user).to receive(:ldap_user?).and_return(true)
end
it { is_expected.to be_allowed(:access_api) }
end
end
context 'when terms are enforced' do
@ -433,6 +449,14 @@ RSpec.describe GlobalPolicy do
end
it { is_expected.not_to be_allowed(:access_git) }
context 'when user is using ldap' do
before do
allow(current_user).to receive(:ldap_user?).and_return(true)
end
it { is_expected.to be_allowed(:access_git) }
end
end
end
@ -517,6 +541,14 @@ RSpec.describe GlobalPolicy do
end
it { is_expected.not_to be_allowed(:use_slash_commands) }
context 'when user is using ldap' do
before do
allow(current_user).to receive(:ldap_user?).and_return(true)
end
it { is_expected.to be_allowed(:use_slash_commands) }
end
end
end

View file

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe 'Setting assignees of a merge request' do
RSpec.describe 'Setting assignees of a merge request', :clean_gitlab_redis_shared_state do
include GraphqlHelpers
let_it_be(:project) { create(:project, :repository) }
@ -46,6 +46,8 @@ RSpec.describe 'Setting assignees of a merge request' do
end
def run_mutation!
post_graphql_mutation(mutation, current_user: current_user) # warm-up
recorder = ActiveRecord::QueryRecorder.new do
post_graphql_mutation(mutation, current_user: current_user)
end
@ -68,7 +70,7 @@ RSpec.describe 'Setting assignees of a merge request' do
context 'when the current user does not have permission to add assignees' do
let(:current_user) { create(:user) }
let(:db_query_limit) { 27 }
let(:db_query_limit) { 28 }
it 'does not change the assignees' do
project.add_guest(current_user)
@ -80,7 +82,7 @@ RSpec.describe 'Setting assignees of a merge request' do
end
context 'with assignees already assigned' do
let(:db_query_limit) { 39 }
let(:db_query_limit) { 46 }
before do
merge_request.assignees = [assignee2]
@ -96,7 +98,7 @@ RSpec.describe 'Setting assignees of a merge request' do
end
context 'when passing an empty list of assignees' do
let(:db_query_limit) { 31 }
let(:db_query_limit) { 33 }
let(:input) { { assignee_usernames: [] } }
before do
@ -115,7 +117,7 @@ RSpec.describe 'Setting assignees of a merge request' do
context 'when passing append as true' do
let(:mode) { Types::MutationOperationModeEnum.enum[:append] }
let(:input) { { assignee_usernames: [assignee2.username], operation_mode: mode } }
let(:db_query_limit) { 20 }
let(:db_query_limit) { 22 }
before do
# In CE, APPEND is a NOOP as you can't have multiple assignees
@ -135,7 +137,7 @@ RSpec.describe 'Setting assignees of a merge request' do
end
context 'when passing remove as true' do
let(:db_query_limit) { 31 }
let(:db_query_limit) { 33 }
let(:mode) { Types::MutationOperationModeEnum.enum[:remove] }
let(:input) { { assignee_usernames: [assignee.username], operation_mode: mode } }
let(:expected_result) { [] }

View file

@ -6,6 +6,9 @@ RSpec.describe 'GraphQL' do
include AfterNextHelpers
let(:query) { graphql_query_for('echo', text: 'Hello world') }
let(:mutation) { 'mutation { echoCreate(input: { messages: ["hello", "world"] }) { echoes } }' }
let_it_be(:user) { create(:user) }
describe 'logging' do
shared_examples 'logging a graphql query' do
@ -70,6 +73,139 @@ RSpec.describe 'GraphQL' do
end
end
context 'when executing mutations' do
let(:mutation_with_variables) do
<<~GQL
mutation($a: String!, $b: String!) {
echoCreate(input: { messages: [$a, $b] }) { echoes }
}
GQL
end
context 'with POST' do
it 'succeeds' do
post_graphql(mutation, current_user: user)
expect(graphql_data_at(:echo_create, :echoes)).to eq %w[hello world]
end
context 'with variables' do
it 'succeeds' do
post_graphql(mutation_with_variables, current_user: user, variables: { a: 'Yo', b: 'there' })
expect(graphql_data_at(:echo_create, :echoes)).to eq %w[Yo there]
end
end
end
context 'with GET' do
it 'fails' do
get_graphql(mutation, current_user: user)
expect(graphql_errors).to include(a_hash_including('message' => /Mutations are forbidden/))
end
context 'with variables' do
it 'fails' do
get_graphql(mutation_with_variables, current_user: user, variables: { a: 'Yo', b: 'there' })
expect(graphql_errors).to include(a_hash_including('message' => /Mutations are forbidden/))
end
end
end
end
context 'when executing queries' do
context 'with POST' do
it 'succeeds' do
post_graphql(query, current_user: user)
expect(graphql_data_at(:echo)).to include 'Hello world'
end
end
context 'with GET' do
it 'succeeds' do
get_graphql(query, current_user: user)
expect(graphql_data_at(:echo)).to include 'Hello world'
end
end
end
context 'when selecting a query by operation name' do
let(:query) { "query A #{graphql_query_for('echo', text: 'Hello world')}" }
let(:mutation) { 'mutation B { echoCreate(input: { messages: ["hello", "world"] }) { echoes } }' }
let(:combined) { [query, mutation].join("\n\n") }
context 'with POST' do
it 'succeeds when selecting the query' do
post_graphql(combined, current_user: user, params: { operationName: 'A' })
resp = json_response
expect(resp.dig('data', 'echo')).to include 'Hello world'
end
it 'succeeds when selecting the mutation' do
post_graphql(combined, current_user: user, params: { operationName: 'B' })
resp = json_response
expect(resp.dig('data', 'echoCreate', 'echoes')).to eq %w[hello world]
end
end
context 'with GET' do
it 'succeeds when selecting the query' do
get_graphql(combined, current_user: user, params: { operationName: 'A' })
resp = json_response
expect(resp.dig('data', 'echo')).to include 'Hello world'
end
it 'fails when selecting the mutation' do
get_graphql(combined, current_user: user, params: { operationName: 'B' })
resp = json_response
expect(resp.dig('errors', 0, 'message')).to include "Mutations are forbidden"
end
end
end
context 'when batching mutations and queries' do
let(:batched) do
[
{ query: "query A #{graphql_query_for('echo', text: 'Hello world')}" },
{ query: 'mutation B { echoCreate(input: { messages: ["hello", "world"] }) { echoes } }' }
]
end
context 'with POST' do
it 'succeeds' do
post_multiplex(batched, current_user: user)
resp = json_response
expect(resp.dig(0, 'data', 'echo')).to include 'Hello world'
expect(resp.dig(1, 'data', 'echoCreate', 'echoes')).to eq %w[hello world]
end
end
context 'with GET' do
it 'fails with a helpful error message' do
get_multiplex(batched, current_user: user)
resp = json_response
expect(resp.dig('errors', 0, 'message')).to include "Mutations are forbidden"
end
end
end
context 'with invalid variables' do
it 'returns an error' do
post_graphql(query, variables: "This is not JSON")
@ -80,8 +216,6 @@ RSpec.describe 'GraphQL' do
end
describe 'authentication', :allow_forgery_protection do
let(:user) { create(:user) }
it 'allows access to public data without authentication' do
post_graphql(query)
@ -109,11 +243,9 @@ RSpec.describe 'GraphQL' do
context 'with token authentication' do
let(:token) { create(:personal_access_token) }
before do
stub_authentication_activity_metrics(debug: false)
end
it 'authenticates users with a PAT' do
stub_authentication_activity_metrics(debug: false)
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
.and increment(:user_session_override_counter)
@ -124,6 +256,14 @@ RSpec.describe 'GraphQL' do
expect(graphql_data['echo']).to eq("\"#{token.user.username}\" says: Hello world")
end
it 'prevents access by deactived users' do
token.user.deactivate!
post_graphql(query, headers: { 'PRIVATE-TOKEN' => token.token })
expect(graphql_errors).to include({ 'message' => /API not accessible/ })
end
context 'when the personal access token has no api scope' do
it 'does not log the user in' do
token.update!(scopes: [:read_user])

View file

@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe API::ImportBitbucketServer do
let(:base_uri) { "https://test:7990" }
let(:user) { create(:user) }
let(:user) { create(:user, bio: 'test') }
let(:token) { "asdasd12345" }
let(:secret) { "sekrettt" }
let(:project_key) { 'TES' }

View file

@ -56,7 +56,7 @@ RSpec.describe API::Projects do
let_it_be(:project, reload: true) { create(:project, :repository, namespace: user.namespace) }
let_it_be(:project2, reload: true) { create(:project, namespace: user.namespace) }
let_it_be(:project_member) { create(:project_member, :developer, user: user3, project: project) }
let_it_be(:user4) { create(:user, username: 'user.with.dot') }
let_it_be(:user4) { create(:user, username: 'user.withdot') }
let_it_be(:project3, reload: true) do
create(:project,
:private,

View file

@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe API::Users do
let_it_be(:admin) { create(:admin) }
let_it_be(:user, reload: true) { create(:user, username: 'user.with.dot') }
let_it_be(:user, reload: true) { create(:user, username: 'user.withdot') }
let_it_be(:key) { create(:key, user: user) }
let_it_be(:gpg_key) { create(:gpg_key, user: user) }
let_it_be(:email) { create(:email, user: user) }

View file

@ -36,16 +36,6 @@ RSpec.describe 'Git HTTP requests' do
end
end
context "when password is expired" do
it "responds to downloads with status 401 Unauthorized" do
user.update!(password_expires_at: 2.days.ago)
download(path, user: user.username, password: user.password) do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
context "when user is blocked" do
let(:user) { create(:user, :blocked) }
@ -68,6 +58,26 @@ RSpec.describe 'Git HTTP requests' do
end
end
shared_examples 'operations are not allowed with expired password' do
context "when password is expired" do
it "responds to downloads with status 401 Unauthorized" do
user.update!(password_expires_at: 2.days.ago)
download(path, user: user.username, password: user.password) do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
it "responds to uploads with status 401 Unauthorized" do
user.update!(password_expires_at: 2.days.ago)
upload(path, user: user.username, password: user.password) do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
end
shared_examples 'pushes require Basic HTTP Authentication' do
context "when no credentials are provided" do
it "responds to uploads with status 401 Unauthorized (no project existence information leak)" do
@ -95,15 +105,6 @@ RSpec.describe 'Git HTTP requests' do
expect(response.header['WWW-Authenticate']).to start_with('Basic ')
end
end
context "when password is expired" do
it "responds to uploads with status 401 Unauthorized" do
user.update!(password_expires_at: 2.days.ago)
upload(path, user: user.username, password: user.password) do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
end
context "when authentication succeeds" do
@ -212,6 +213,7 @@ RSpec.describe 'Git HTTP requests' do
it_behaves_like 'pulls require Basic HTTP Authentication'
it_behaves_like 'pushes require Basic HTTP Authentication'
it_behaves_like 'operations are not allowed with expired password'
context 'when authenticated' do
it 'rejects downloads and uploads with 404 Not Found' do
@ -306,6 +308,7 @@ RSpec.describe 'Git HTTP requests' do
it_behaves_like 'pulls require Basic HTTP Authentication'
it_behaves_like 'pushes require Basic HTTP Authentication'
it_behaves_like 'operations are not allowed with expired password'
context 'when authenticated' do
context 'and as a developer on the team' do
@ -473,6 +476,7 @@ RSpec.describe 'Git HTTP requests' do
it_behaves_like 'pulls require Basic HTTP Authentication'
it_behaves_like 'pushes require Basic HTTP Authentication'
it_behaves_like 'operations are not allowed with expired password'
end
context 'but the repo is enabled' do
@ -488,6 +492,7 @@ RSpec.describe 'Git HTTP requests' do
it_behaves_like 'pulls require Basic HTTP Authentication'
it_behaves_like 'pushes require Basic HTTP Authentication'
it_behaves_like 'operations are not allowed with expired password'
end
end
@ -508,6 +513,7 @@ RSpec.describe 'Git HTTP requests' do
it_behaves_like 'pulls require Basic HTTP Authentication'
it_behaves_like 'pushes require Basic HTTP Authentication'
it_behaves_like 'operations are not allowed with expired password'
context "when username and password are provided" do
let(:env) { { user: user.username, password: 'nope' } }
@ -1003,6 +1009,24 @@ RSpec.describe 'Git HTTP requests' do
it_behaves_like 'pulls are allowed'
it_behaves_like 'pushes are allowed'
context "when password is expired" do
it "responds to downloads with status 200" do
user.update!(password_expires_at: 2.days.ago)
download(path, user: user.username, password: user.password) do |response|
expect(response).to have_gitlab_http_status(:ok)
end
end
it "responds to uploads with status 200" do
user.update!(password_expires_at: 2.days.ago)
upload(path, user: user.username, password: user.password) do |response|
expect(response).to have_gitlab_http_status(:ok)
end
end
end
end
end
end

View file

@ -3,7 +3,14 @@
require 'spec_helper'
RSpec.describe IdeController do
let_it_be(:project) { create(:project, :public) }
let_it_be(:reporter) { create(:user) }
let_it_be(:project) do
create(:project, :private).tap do |p|
p.add_reporter(reporter)
end
end
let_it_be(:creator) { project.creator }
let_it_be(:other_user) { create(:user) }
@ -14,48 +21,62 @@ RSpec.describe IdeController do
sign_in(user)
end
it 'increases the views counter' do
expect(Gitlab::UsageDataCounters::WebIdeCounter).to receive(:increment_views_count)
get ide_url
end
describe '#index', :aggregate_failures do
subject { get route }
shared_examples 'user cannot push code' do
include ProjectForksHelper
shared_examples 'user access rights check' do
context 'user can read project' do
it 'increases the views counter' do
expect(Gitlab::UsageDataCounters::WebIdeCounter).to receive(:increment_views_count)
let(:user) { other_user }
context 'when user does not have fork' do
it 'instantiates fork_info instance var with fork_path and return 200' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:project)).to eq project
expect(assigns(:fork_info)).to eq({ fork_path: controller.helpers.ide_fork_and_edit_path(project, branch, '', with_notice: false) })
end
it 'has nil fork_info if user cannot fork' do
project.project_feature.update!(forking_access_level: ProjectFeature::DISABLED)
context 'user can read project but cannot push code' do
include ProjectForksHelper
subject
let(:user) { reporter }
expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:fork_info)).to be_nil
context 'when user does not have fork' do
it 'instantiates fork_info instance var with fork_path and returns 200' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:project)).to eq project
expect(assigns(:fork_info)).to eq({ fork_path: controller.helpers.ide_fork_and_edit_path(project, branch, '', with_notice: false) })
end
it 'has nil fork_info if user cannot fork' do
project.project_feature.update!(forking_access_level: ProjectFeature::DISABLED)
subject
expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:fork_info)).to be_nil
end
end
context 'when user has fork' do
let!(:fork) { fork_project(project, user, repository: true, namespace: user.namespace) }
it 'instantiates fork_info instance var with ide_path and returns 200' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:project)).to eq project
expect(assigns(:fork_info)).to eq({ ide_path: controller.helpers.ide_edit_path(fork, branch, '') })
end
end
end
end
context 'when user has fork' do
let!(:fork) { fork_project(project, user, repository: true, namespace: user.namespace) }
context 'user cannot read project' do
let(:user) { other_user }
it 'instantiates fork_info instance var with ide_path and return 200' do
it 'returns 404' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:project)).to eq project
expect(assigns(:fork_info)).to eq({ ide_path: controller.helpers.ide_edit_path(fork, branch, '') })
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
@ -63,37 +84,27 @@ RSpec.describe IdeController do
context '/-/ide' do
let(:route) { '/-/ide' }
it 'does not instantiate any instance var and return 200' do
it 'returns 404' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:project)).to be_nil
expect(assigns(:branch)).to be_nil
expect(assigns(:path)).to be_nil
expect(assigns(:merge_request)).to be_nil
expect(assigns(:fork_info)).to be_nil
expect(response).to have_gitlab_http_status(:not_found)
end
end
context '/-/ide/project' do
let(:route) { '/-/ide/project' }
it 'does not instantiate any instance var and return 200' do
it 'returns 404' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:project)).to be_nil
expect(assigns(:branch)).to be_nil
expect(assigns(:path)).to be_nil
expect(assigns(:merge_request)).to be_nil
expect(assigns(:fork_info)).to be_nil
expect(response).to have_gitlab_http_status(:not_found)
end
end
context '/-/ide/project/:project' do
let(:route) { "/-/ide/project/#{project.full_path}" }
it 'instantiates project instance var and return 200' do
it 'instantiates project instance var and returns 200' do
subject
expect(response).to have_gitlab_http_status(:ok)
@ -104,13 +115,13 @@ RSpec.describe IdeController do
expect(assigns(:fork_info)).to be_nil
end
it_behaves_like 'user cannot push code'
it_behaves_like 'user access rights check'
%w(edit blob tree).each do |action|
context "/-/ide/project/:project/#{action}" do
let(:route) { "/-/ide/project/#{project.full_path}/#{action}" }
it 'instantiates project instance var and return 200' do
it 'instantiates project instance var and returns 200' do
subject
expect(response).to have_gitlab_http_status(:ok)
@ -121,13 +132,13 @@ RSpec.describe IdeController do
expect(assigns(:fork_info)).to be_nil
end
it_behaves_like 'user cannot push code'
it_behaves_like 'user access rights check'
context "/-/ide/project/:project/#{action}/:branch" do
let(:branch) { 'master' }
let(:route) { "/-/ide/project/#{project.full_path}/#{action}/#{branch}" }
it 'instantiates project and branch instance vars and return 200' do
it 'instantiates project and branch instance vars and returns 200' do
subject
expect(response).to have_gitlab_http_status(:ok)
@ -138,13 +149,13 @@ RSpec.describe IdeController do
expect(assigns(:fork_info)).to be_nil
end
it_behaves_like 'user cannot push code'
it_behaves_like 'user access rights check'
context "/-/ide/project/:project/#{action}/:branch/-" do
let(:branch) { 'branch/slash' }
let(:route) { "/-/ide/project/#{project.full_path}/#{action}/#{branch}/-" }
it 'instantiates project and branch instance vars and return 200' do
it 'instantiates project and branch instance vars and returns 200' do
subject
expect(response).to have_gitlab_http_status(:ok)
@ -155,13 +166,13 @@ RSpec.describe IdeController do
expect(assigns(:fork_info)).to be_nil
end
it_behaves_like 'user cannot push code'
it_behaves_like 'user access rights check'
context "/-/ide/project/:project/#{action}/:branch/-/:path" do
let(:branch) { 'master' }
let(:route) { "/-/ide/project/#{project.full_path}/#{action}/#{branch}/-/foo/.bar" }
it 'instantiates project, branch, and path instance vars and return 200' do
it 'instantiates project, branch, and path instance vars and returns 200' do
subject
expect(response).to have_gitlab_http_status(:ok)
@ -172,7 +183,7 @@ RSpec.describe IdeController do
expect(assigns(:fork_info)).to be_nil
end
it_behaves_like 'user cannot push code'
it_behaves_like 'user access rights check'
end
end
end
@ -184,7 +195,7 @@ RSpec.describe IdeController do
let(:route) { "/-/ide/project/#{project.full_path}/merge_requests/#{merge_request.id}" }
it 'instantiates project and merge_request instance vars and return 200' do
it 'instantiates project and merge_request instance vars and returns 200' do
subject
expect(response).to have_gitlab_http_status(:ok)
@ -195,7 +206,7 @@ RSpec.describe IdeController do
expect(assigns(:fork_info)).to be_nil
end
it_behaves_like 'user cannot push code'
it_behaves_like 'user access rights check'
end
end
end

View file

@ -68,12 +68,12 @@ RSpec.describe FeatureFlags::CreateService do
end
it 'creates audit event' do
expected_message = 'Created feature flag <strong>feature_flag</strong> '\
'with description <strong>"description"</strong>. '\
'Created rule <strong>*</strong> and set it as <strong>active</strong> '\
'with strategies <strong>[{"name"=>"default", "parameters"=>{}}]</strong>. '\
'Created rule <strong>production</strong> and set it as <strong>inactive</strong> '\
'with strategies <strong>[{"name"=>"default", "parameters"=>{}}]</strong>.'
expected_message = 'Created feature flag feature_flag '\
'with description "description". '\
'Created rule * and set it as active '\
'with strategies [{"name"=&gt;"default", "parameters"=&gt;{}}]. '\
'Created rule production and set it as inactive '\
'with strategies [{"name"=&gt;"default", "parameters"=&gt;{}}].'
expect { subject }.to change { AuditEvent.count }.by(1)
expect(AuditEvent.last.details[:custom_message]).to eq(expected_message)

View file

@ -33,7 +33,7 @@ RSpec.describe FeatureFlags::DestroyService do
it 'creates audit log' do
expect { subject }.to change { AuditEvent.count }.by(1)
expect(audit_event_message).to eq("Deleted feature flag <strong>#{feature_flag.name}</strong>.")
expect(audit_event_message).to eq("Deleted feature flag #{feature_flag.name}.")
end
context 'when user is reporter' do

View file

@ -38,9 +38,9 @@ RSpec.describe FeatureFlags::UpdateService do
expect { subject }.to change { AuditEvent.count }.by(1)
expect(audit_event_message).to(
eq("Updated feature flag <strong>new_name</strong>. "\
"Updated name from <strong>\"#{name_was}\"</strong> "\
"to <strong>\"new_name\"</strong>.")
eq("Updated feature flag new_name. "\
"Updated name from \"#{name_was}\" "\
"to \"new_name\".")
)
end
@ -94,8 +94,8 @@ RSpec.describe FeatureFlags::UpdateService do
it 'creates audit event with changed description' do
expect { subject }.to change { AuditEvent.count }.by(1)
expect(audit_event_message).to(
include("Updated description from <strong>\"\"</strong>"\
" to <strong>\"new description\"</strong>.")
include("Updated description from \"\""\
" to \"new description\".")
)
end
end
@ -110,7 +110,7 @@ RSpec.describe FeatureFlags::UpdateService do
it 'creates audit event about changing active state' do
expect { subject }.to change { AuditEvent.count }.by(1)
expect(audit_event_message).to(
include('Updated active from <strong>"true"</strong> to <strong>"false"</strong>.')
include('Updated active from "true" to "false".')
)
end
@ -132,8 +132,8 @@ RSpec.describe FeatureFlags::UpdateService do
it 'creates audit event about changing active state' do
expect { subject }.to change { AuditEvent.count }.by(1)
expect(audit_event_message).to(
include("Updated rule <strong>*</strong> active state "\
"from <strong>true</strong> to <strong>false</strong>.")
include("Updated rule * active state "\
"from true to false.")
)
end
end
@ -149,8 +149,8 @@ RSpec.describe FeatureFlags::UpdateService do
it 'creates audit event with changed name' do
expect { subject }.to change { AuditEvent.count }.by(1)
expect(audit_event_message).to(
include("Updated rule <strong>staging</strong> environment scope "\
"from <strong>review</strong> to <strong>staging</strong>.")
include("Updated rule staging environment scope "\
"from review to staging.")
)
end
@ -185,7 +185,7 @@ RSpec.describe FeatureFlags::UpdateService do
it 'creates audit event with deleted scope' do
expect { subject }.to change { AuditEvent.count }.by(1)
expect(audit_event_message).to include("Deleted rule <strong>review</strong>.")
expect(audit_event_message).to include("Deleted rule review.")
end
context 'when scope can not be deleted' do
@ -210,8 +210,8 @@ RSpec.describe FeatureFlags::UpdateService do
end
it 'creates audit event with new scope' do
expected = 'Created rule <strong>review</strong> and set it as <strong>active</strong> '\
'with strategies <strong>[{"name"=>"default", "parameters"=>{}}]</strong>.'
expected = 'Created rule review and set it as active '\
'with strategies [{"name"=&gt;"default", "parameters"=&gt;{}}].'
subject
@ -260,7 +260,7 @@ RSpec.describe FeatureFlags::UpdateService do
end
it 'creates an audit event' do
expected = %r{Updated rule <strong>sandbox</strong> strategies from <strong>.*</strong> to <strong>.*</strong>.}
expected = %r{Updated rule sandbox strategies from .* to .*.}
expect { subject }.to change { AuditEvent.count }.by(1)
expect(audit_event_message).to match(expected)

View file

@ -184,14 +184,6 @@ RSpec.describe Projects::ForkService do
end
end
context 'GitLab CI is enabled' do
it "forks and enables CI for fork" do
@from_project.enable_ci
@to_project = fork_project(@from_project, @to_user, using_service: true)
expect(@to_project.builds_enabled?).to be_truthy
end
end
context "CI/CD settings" do
let(:to_project) { fork_project(@from_project, @to_user, using_service: true) }
@ -366,6 +358,19 @@ RSpec.describe Projects::ForkService do
expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
end
it 'copies project features visibility settings to the fork', :aggregate_failures do
attrs = ProjectFeature::FEATURES.to_h do |f|
["#{f}_access_level", ProjectFeature::PRIVATE]
end
public_project.project_feature.update!(attrs)
user = create(:user, developer_projects: [public_project])
forked_project = described_class.new(public_project, user).execute
expect(forked_project.project_feature.slice(attrs.keys)).to eq(attrs)
end
end
end

View file

@ -76,7 +76,7 @@ RSpec.describe Repositories::ChangelogService do
recorder = ActiveRecord::QueryRecorder.new { service.execute }
changelog = project.repository.blob_at('master', 'CHANGELOG.md')&.data
expect(recorder.count).to eq(11)
expect(recorder.count).to eq(12)
expect(changelog).to include('Title 1', 'Title 2')
end

View file

@ -396,8 +396,13 @@ module GraphqlHelpers
post api('/', current_user, version: 'graphql'), params: { _json: queries }, headers: headers
end
def post_graphql(query, current_user: nil, variables: nil, headers: {}, token: {})
params = { query: query, variables: serialize_variables(variables) }
def get_multiplex(queries, current_user: nil, headers: {})
path = "/?#{queries.to_query('_json')}"
get api(path, current_user, version: 'graphql'), headers: headers
end
def post_graphql(query, current_user: nil, variables: nil, headers: {}, token: {}, params: {})
params = params.merge(query: query, variables: serialize_variables(variables))
post api('/', current_user, version: 'graphql', **token), params: params, headers: headers
return unless graphql_errors
@ -406,6 +411,18 @@ module GraphqlHelpers
expect(graphql_errors).not_to include(a_hash_including('message' => 'Internal server error'))
end
def get_graphql(query, current_user: nil, variables: nil, headers: {}, token: {}, params: {})
vars = "variables=#{CGI.escape(serialize_variables(variables))}" if variables
params = params.to_a.map { |k, v| v.to_query(k) }
path = ["/?query=#{CGI.escape(query)}", vars, *params].join('&')
get api(path, current_user, version: 'graphql', **token), headers: headers
return unless graphql_errors
# Errors are acceptable, but not this one:
expect(graphql_errors).not_to include(a_hash_including('message' => 'Internal server error'))
end
def post_graphql_mutation(mutation, current_user: nil, token: {})
post_graphql(mutation.query,
current_user: current_user,