Update upstream source from tag 'upstream/13.12.6+ds1'

Update to upstream version '13.12.6+ds1'
with Debian dir 492090d23c
This commit is contained in:
Pirate Praveen 2021-07-02 01:07:42 +05:30
commit a0e6cc2626
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 documentation](doc/development/changelog.md) for instructions on adding your own
entry. 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) ## 13.12.4 (2021-06-14)
### Fixed (3 changes) ### Fixed (3 changes)

View file

@ -1 +1 @@
13.12.4 13.12.6

View file

@ -2,7 +2,7 @@
source 'https://rubygems.org' source 'https://rubygems.org'
gem 'rails', '~> 6.0.3.6' gem 'rails', '~> 6.0.3.7'
gem 'bootsnap', '~> 1.4.6' gem 'bootsnap', '~> 1.4.6'
@ -157,7 +157,7 @@ gem 'github-markup', '~> 1.7.0', require: 'github/markup'
gem 'commonmarker', '~> 0.21' gem 'commonmarker', '~> 0.21'
gem 'kramdown', '~> 2.3.1' gem 'kramdown', '~> 2.3.1'
gem 'RedCloth', '~> 4.3.2' 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 'org-ruby', '~> 0.9.12'
gem 'creole', '~> 0.5.0' gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1' gem 'wikicloth', '0.8.1'
@ -168,7 +168,7 @@ gem 'asciidoctor-kroki', '~> 0.4.0', require: false
gem 'rouge', '~> 3.26.0' gem 'rouge', '~> 3.26.0'
gem 'truncato', '~> 0.7.11' gem 'truncato', '~> 0.7.11'
gem 'bootstrap_form', '~> 4.2.0' gem 'bootstrap_form', '~> 4.2.0'
gem 'nokogiri', '~> 1.11.1' gem 'nokogiri', '~> 1.11.4'
gem 'escape_utils', '~> 1.1' gem 'escape_utils', '~> 1.1'
# Calendar rendering # Calendar rendering

View file

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

View file

@ -1 +1 @@
13.12.4 13.12.6

View file

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

View file

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

View file

@ -1,5 +1,5 @@
<template> <template>
<div class="nothing-here-block"> <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> </div>
</template> </template>

View file

@ -208,7 +208,10 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
params[:application_setting][:import_sources]&.delete("") params[:application_setting][:import_sources]&.delete("")
params[:application_setting][:restricted_visibility_levels]&.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) 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_denylist_raw) if params[:domain_denylist]
params.delete(:domain_allowlist_raw) if params[:domain_allowlist] params.delete(:domain_allowlist_raw) if params[:domain_allowlist]
params.require(:application_setting).permit( params[:application_setting].permit(visible_application_setting_attributes)
visible_application_setting_attributes
)
end end
def recheck_user_consent? def recheck_user_consent?

View file

@ -108,7 +108,7 @@ module MembershipActions
respond_to do |format| respond_to do |format|
format.html do 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 redirect_to redirect_path, notice: notice
end end

View file

@ -20,12 +20,16 @@ class GraphqlController < ApplicationController
# around in GraphiQL. # around in GraphiQL.
protect_from_forgery with: :null_session, only: :execute 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(only: [:execute]) { authenticate_sessionless_user!(:api) }
before_action :authorize_access_api!
before_action :set_user_last_activity before_action :set_user_last_activity
before_action :track_vs_code_usage before_action :track_vs_code_usage
before_action :disable_query_limiting before_action :disable_query_limiting
before_action :disallow_mutations_for_get
# Since we deactivate authentication from the main ApplicationController and # Since we deactivate authentication from the main ApplicationController and
# defer it to :authorize_access_api!, we need to override the bypass session # defer it to :authorize_access_api!, we need to override the bypass session
# callback execution order here # callback execution order here
@ -62,6 +66,25 @@ class GraphqlController < ApplicationController
private 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 # Tests may mark some GraphQL queries as exempt from SQL query limits
def disable_query_limiting def disable_query_limiting
return unless Gitlab::QueryLimiting.enabled_for_env? return unless Gitlab::QueryLimiting.enabled_for_env?
@ -130,7 +153,9 @@ class GraphqlController < ApplicationController
end end
def authorize_access_api! 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 end
# Overridden from the ApplicationController to make the response look like # Overridden from the ApplicationController to make the response look like

View file

@ -7,6 +7,8 @@ class IdeController < ApplicationController
include StaticObjectExternalStorageCSP include StaticObjectExternalStorageCSP
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
before_action :authorize_read_project!
before_action do before_action do
push_frontend_feature_flag(:build_service_proxy) push_frontend_feature_flag(:build_service_proxy)
push_frontend_feature_flag(:schema_linting) push_frontend_feature_flag(:schema_linting)
@ -22,6 +24,10 @@ class IdeController < ApplicationController
private private
def authorize_read_project!
render_404 unless can?(current_user, :read_project, project)
end
def define_index_vars def define_index_vars
return unless project 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::Ci::Job::Retry
mount_mutation Mutations::Namespace::PackageSettings::Update mount_mutation Mutations::Namespace::PackageSettings::Update
mount_mutation Mutations::UserCallouts::Create mount_mutation Mutations::UserCallouts::Create
mount_mutation Mutations::Echo
end end
end end

View file

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

View file

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

View file

@ -50,9 +50,11 @@ class DroneCiService < CiService
end end
def calculate_reactive_cache(sha, ref) 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, verify: enable_ssl_verification,
extra_log_info: { project_id: project_id }) extra_log_info: { project_id: project_id },
use_read_total_timeout: true)
status = status =
if response && response.code == 200 && response['status'] if response && response.code == 200 && response['status']

View file

@ -38,7 +38,7 @@ class ExternalWikiService < Integration
end end
def execute(_data) 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 response.body if response.code == 200
rescue StandardError rescue StandardError
nil nil

View file

@ -106,7 +106,7 @@ class IssueTrackerService < Integration
result = false result = false
begin 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 if response
message = "#{self.type} received response #{response.code} when attempting to connect to #{self.project_url}" 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) 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) read_commit_status(response)
rescue Errno::ECONNREFUSED rescue Errno::ECONNREFUSED
:error :error

View file

@ -17,7 +17,7 @@ module SlackMattermost
class HTTPClient class HTTPClient
def self.post(uri, params = {}) def self.post(uri, params = {})
params.delete(:http_options) # these are internal to the client and we do not want them 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 end
end end

View file

@ -169,7 +169,7 @@ class TeamcityService < CiService
end end
def get_path(path) 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 end
def post_to_build_queue(data, branch) def post_to_build_queue(data, branch)
@ -179,7 +179,8 @@ class TeamcityService < CiService
"<buildType id=#{build_type.encode(xml: :attr)}/>"\ "<buildType id=#{build_type.encode(xml: :attr)}/>"\
'</build>', '</build>',
headers: { 'Content-type' => 'application/xml' }, headers: { 'Content-type' => 'application/xml' },
basic_auth: basic_auth basic_auth: basic_auth,
use_read_total_timeout: true
) )
end end

View file

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

View file

@ -43,7 +43,7 @@ class WebexTeamsService < ChatNotificationService
def notify(message, opts) def notify(message, opts)
header = { 'Content-Type' => 'application/json' } 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? response if response.success?
end end

View file

@ -20,7 +20,7 @@ class ProtectedBranch::PushAccessLevel < ApplicationRecord
def check_access(user) def check_access(user)
if user && deploy_key.present? 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 end
super super

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
- add_page_specific_style 'page_bundles/import' - 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, {}) - extra_data = local_assigns.fetch(:extra_data, {})
- filterable = local_assigns.fetch(:filterable, true) - filterable = local_assigns.fetch(:filterable, true)
- paginatable = local_assigns.fetch(:paginatable, false) - paginatable = local_assigns.fetch(:paginatable, false)

View file

@ -1,2 +1,2 @@
.nothing-here-block .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 - labels = issuable.labels
- assignees = issuable.assignees - assignees = issuable.assignees
- base_url_args = [project] - 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] - issuable_url_args = base_url_args + [issuable]
%li.issuable-row %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="mutationdismissvulnerabilityerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationdismissvulnerabilityvulnerability"></a>`vulnerability` | [`Vulnerability`](#vulnerability) | The vulnerability after dismissal. | | <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` ### `Mutation.environmentsCanaryIngressUpdate`
Input type: `EnvironmentsCanaryIngressUpdateInput` Input type: `EnvironmentsCanaryIngressUpdateInput`

View file

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

View file

@ -382,7 +382,7 @@ module Gitlab
end end
def can_user_login_with_non_expired_password?(user) 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 end
end end

View file

@ -23,6 +23,8 @@ module Gitlab
"Your primary email address is not confirmed. "\ "Your primary email address is not confirmed. "\
"Please check your inbox for the confirmation instructions. "\ "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}" "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 when :password_expired
"Your password expired. "\ "Your password expired. "\
"Please access GitLab from a web browser to update your password." "Please access GitLab from a web browser to update your password."
@ -44,6 +46,8 @@ module Gitlab
:deactivated :deactivated
elsif !@user.confirmed? elsif !@user.confirmed?
:unconfirmed :unconfirmed
elsif @user.blocked?
:blocked
elsif @user.password_expired? elsif @user.password_expired?
:password_expired :password_expired
else else

View file

@ -250,7 +250,7 @@ module Gitlab
end end
def diffable? def diffable?
repository.attributes(file_path).fetch('diff') { true } diffable_by_attribute? && !text_with_binary_notice?
end end
def binary_in_repo? def binary_in_repo?
@ -366,6 +366,15 @@ module Gitlab
private 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) def fetch_blob(sha, path)
return unless sha return unless sha

View file

@ -6,7 +6,7 @@ module Gitlab
include Enumerable include Enumerable
def parse(lines, diff_file: nil) def parse(lines, diff_file: nil)
return [] if lines.blank? return [] if lines.blank? || Git::Diff.has_binary_notice?(lines.first)
@lines = lines @lines = lines
line_obj_index = 0 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 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 class << self
def between(repo, head, base, options = {}, *paths) def between(repo, head, base, options = {}, *paths)
straight = options.delete(:straight) || false straight = options.delete(:straight) || false
@ -131,8 +133,13 @@ module Gitlab
def patch_hard_limit_bytes def patch_hard_limit_bytes
Gitlab::CurrentSettings.diff_max_patch_bytes Gitlab::CurrentSettings.diff_max_patch_bytes
end 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) def initialize(raw_diff, expanded: true)
@expanded = expanded @expanded = expanded
@ -215,7 +222,7 @@ module Gitlab
end end
def has_binary_notice? def has_binary_notice?
@diff.start_with?('Binary') self.class.has_binary_notice?(@diff)
end end
private private

View file

@ -8,9 +8,10 @@ module Gitlab
class HTTP class HTTP
BlockedUrlError = Class.new(StandardError) BlockedUrlError = Class.new(StandardError)
RedirectionTooDeep = Class.new(StandardError) RedirectionTooDeep = Class.new(StandardError)
ReadTotalTimeout = Class.new(Net::ReadTimeout)
HTTP_TIMEOUT_ERRORS = [ HTTP_TIMEOUT_ERRORS = [
Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout, Gitlab::HTTP::ReadTotalTimeout
].freeze ].freeze
HTTP_ERRORS = HTTP_TIMEOUT_ERRORS + [ HTTP_ERRORS = HTTP_TIMEOUT_ERRORS + [
SocketError, OpenSSL::SSL::SSLError, OpenSSL::OpenSSLError, SocketError, OpenSSL::SSL::SSLError, OpenSSL::OpenSSLError,
@ -23,6 +24,7 @@ module Gitlab
read_timeout: 20, read_timeout: 20,
write_timeout: 30 write_timeout: 30
}.freeze }.freeze
DEFAULT_READ_TOTAL_TIMEOUT = 20.seconds
include HTTParty # rubocop:disable Gitlab/HTTParty include HTTParty # rubocop:disable Gitlab/HTTParty
@ -41,7 +43,19 @@ module Gitlab
options options
end 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 rescue HTTParty::RedirectionTooDeep
raise RedirectionTooDeep raise RedirectionTooDeep
rescue *HTTP_ERRORS => e rescue *HTTP_ERRORS => e

View file

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

View file

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

View file

@ -44,7 +44,7 @@ RSpec.describe GraphqlController do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
end 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 # User cannot access API in a couple of cases
# * When user is internal(like ghost users) # * When user is internal(like ghost users)
# * When user is blocked # * When user is blocked
@ -54,7 +54,9 @@ RSpec.describe GraphqlController do
post :execute post :execute
expect(response).to have_gitlab_http_status(:forbidden) 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 end
it 'updates the users last_activity_on field' do it 'updates the users last_activity_on field' do

View file

@ -55,9 +55,9 @@ module DeprecationToolkitEnv
# one by one # one by one
def self.allowed_kwarg_warning_paths def self.allowed_kwarg_warning_paths
%w[ %w[
activerecord-6.0.3.6/lib/active_record/migration.rb activerecord-6.0.3.7/lib/active_record/migration.rb
activesupport-6.0.3.6/lib/active_support/cache.rb activesupport-6.0.3.7/lib/active_support/cache.rb
activerecord-6.0.3.6/lib/active_record/relation.rb activerecord-6.0.3.7/lib/active_record/relation.rb
asciidoctor-2.0.12/lib/asciidoctor/extensions.rb asciidoctor-2.0.12/lib/asciidoctor/extensions.rb
attr_encrypted-3.1.0/lib/attr_encrypted/adapters/active_record.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') click_link('Expand all')
# Wait for elements to appear to ensure full page reload # 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('This source diff could not be displayed because it is too large.')
expect(page).to have_content('too_large_image.jpg') expect(page).to have_content('too_large_image.jpg')
find('.note-textarea') find('.note-textarea')

View file

@ -174,4 +174,14 @@ RSpec.describe 'Diff file viewer', :js, :with_clean_rails_cache do
end end
end 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 end

View file

@ -65,18 +65,6 @@ RSpec.describe 'Comments on personal snippets', :js do
expect(page).to have_content(user_name) expect(page).to have_content(user_name)
end end
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 end
context 'when submitting a note' do context 'when submitting a note' do

View file

@ -1,50 +1,54 @@
import initCopyAsGFM, { CopyAsGFM } from '~/behaviors/markdown/copy_as_gfm'; import initCopyAsGFM, { CopyAsGFM } from '~/behaviors/markdown/copy_as_gfm';
import * as commonUtils from '~/lib/utils/common_utils';
describe('CopyAsGFM', () => { describe('CopyAsGFM', () => {
describe('CopyAsGFM.pasteGFM', () => { 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 = { const e = {
originalEvent: { originalEvent: {
clipboardData: { clipboardData: {
getData(mimeType) { getData(mimeType) {
// When GFM code is copied, we put the regular plain text return data[mimeType] || null;
// 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;
}, },
}, },
}, },
preventDefault() {}, preventDefault() {},
target,
}; };
CopyAsGFM.pasteGFM(e); CopyAsGFM.pasteGFM(e);
} }
it('wraps pasted code when not already in code tags', () => { 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(); callPasteGFM();
expect(target.value).toBe('This is code: `code`');
}); });
it('does not wrap pasted code when already in code tags', () => { it('does not wrap pasted code when already in code tags', () => {
jest.spyOn(commonUtils, 'insertText').mockImplementation((el, textFunc) => { target.value = 'This is code: `';
const insertedText = textFunc('This is code: `', '`');
expect(insertedText).toEqual('code');
});
callPasteGFM(); 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'; import * as urlUtils from '~/lib/utils/url_utility';
const shas = { const shas = {
@ -921,4 +922,37 @@ describe('URL utility', () => {
expect(urlUtils.encodeSaferUrl(input)).toBe(input); 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 { merge } from 'lodash';
import Vuex from 'vuex'; import Vuex from 'vuex';
import { getJSONFixture } from 'helpers/fixtures'; import { getJSONFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
import * as commonUtils from '~/lib/utils/common_utils'; import * as commonUtils from '~/lib/utils/common_utils';
import ReleaseEditNewApp from '~/releases/components/app_edit_new.vue'; import ReleaseEditNewApp from '~/releases/components/app_edit_new.vue';
import AssetLinksForm from '~/releases/components/asset_links_form.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 originalRelease = getJSONFixture('api/releases/release.json');
const originalMilestones = originalRelease.milestones; const originalMilestones = originalRelease.milestones;
const releasesPagePath = 'path/to/releases/page';
describe('Release edit/new component', () => { describe('Release edit/new component', () => {
let wrapper; let wrapper;
@ -24,7 +26,7 @@ describe('Release edit/new component', () => {
state = { state = {
release, release,
markdownDocsPath: 'path/to/markdown/docs', markdownDocsPath: 'path/to/markdown/docs',
releasesPagePath: 'path/to/releases/page', releasesPagePath,
projectId: '8', projectId: '8',
groupId: '42', groupId: '42',
groupMilestonesAvailable: true, groupMilestonesAvailable: true,
@ -75,6 +77,8 @@ describe('Release edit/new component', () => {
}; };
beforeEach(() => { beforeEach(() => {
global.jsdom.reconfigure({ url: TEST_HOST });
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
gon.api_version = 'v4'; gon.api_version = 'v4';
@ -146,22 +150,33 @@ describe('Release edit/new component', () => {
}); });
}); });
describe(`when the URL contains a "${BACK_URL_PARAM}" parameter`, () => { // eslint-disable-next-line no-script-url
const backUrl = 'https://example.gitlab.com/back/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 () => { await factory();
commonUtils.getParameterByName = jest });
.fn()
.mockImplementation((paramToGet) => ({ [BACK_URL_PARAM]: backUrl }[paramToGet]));
await factory(); it(`renders a "Cancel" button with an href pointing to ${expectedHref}`, () => {
}); const cancelButton = wrapper.find('.js-cancel-button');
expect(cancelButton.attributes().href).toBe(expectedHref);
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); );
});
});
describe('when creating a new release', () => { describe('when creating a new release', () => {
beforeEach(async () => { 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 it 'does not highlight files marked as undiffable in .gitattributes' do
allow_next_instance_of(Gitlab::Diff::File) do |instance| 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 end
expect_next_instance_of(Gitlab::Diff::File) do |instance| expect_next_instance_of(Gitlab::Diff::File) do |instance|

View file

@ -186,26 +186,46 @@ RSpec.describe Gitlab::Diff::File do
end end
describe '#diffable?' do describe '#diffable?' do
let(:commit) { project.commit('1a0b36b3cdad1d2ee32457c102a8c0b7056fa863') } context 'when attributes exist' do
let(:diffs) { commit.diffs } let(:commit) { project.commit('1a0b36b3cdad1d2ee32457c102a8c0b7056fa863') }
let(:diffs) { commit.diffs }
before do before do
info_dir_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do info_dir_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
File.join(project.repository.path_to_repo, 'info') 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 end
FileUtils.mkdir(info_dir_path) unless File.exist?(info_dir_path) it "returns true for files that do not have attributes" do
File.write(File.join(info_dir_path, 'attributes'), "*.md -diff\n") 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 end
it "returns true for files that do not have attributes" do context 'when the text has binary notice' do
diff_file = diffs.diff_file_with_new_path('LICENSE') let(:commit) { project.commit('f05a98786e4274708e1fa118c7ad3a29d1d1b9a3') }
expect(diff_file.diffable?).to be_truthy let(:diff_file) { commit.diffs.diff_file_with_new_path('VERSION') }
it "returns false" do
expect(diff_file.diffable?).to be_falsey
end
end end
it "returns false for files that have been marked as not being diffable in attributes" do context 'when the content is binary' do
diff_file = diffs.diff_file_with_new_path('README.md') let(:commit) { project.commit('2f63565e7aac07bcdadb654e253078b727143ec4') }
expect(diff_file.diffable?).to be_falsey 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
end end
@ -729,6 +749,18 @@ RSpec.describe Gitlab::Diff::File do
end end
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 describe '#diff_hunk' do
context 'when first line is a match' do context 'when first line is a match' do
let(:raw_diff) do let(:raw_diff) do

View file

@ -146,6 +146,16 @@ eos
it { expect(parser.parse(nil)).to eq([]) } it { expect(parser.parse(nil)).to eq([]) }
end 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 describe 'tolerates special diff markers in a content' do
it "counts lines correctly" do it "counts lines correctly" do
diff = <<~END 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.") expect { pull_access_check }.to raise_forbidden("Your password expired. Please access GitLab from a web browser to update your password.")
end 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 context 'when the project repository does not exist' do
before do before do
project.add_guest(user) project.add_guest(user)
@ -977,12 +985,26 @@ RSpec.describe Gitlab::GitAccess do
end end
it 'disallows users with expired password to push' do it 'disallows users with expired password to push' do
project.add_maintainer(user)
user.update!(password_expires_at: 2.minutes.ago) 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.") expect { push_access_check }.to raise_forbidden("Your password expired. Please access GitLab from a web browser to update your password.")
end 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 it 'cleans up the files' do
expect(project.repository).to receive(:clean_stale_repository_files).and_call_original expect(project.repository).to receive(:clean_stale_repository_files).and_call_original
expect { push_access_check }.not_to raise_error expect { push_access_check }.not_to raise_error

View file

@ -27,6 +27,47 @@ RSpec.describe Gitlab::HTTP do
end end
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 describe 'allow_local_requests_from_web_hooks_and_services is' do
before do before do
WebMock.stub_request(:get, /.*/).to_return(status: 200, body: 'Success') WebMock.stub_request(:get, /.*/).to_return(status: 200, body: 'Success')

View file

@ -3,9 +3,6 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe AuditEvent do RSpec.describe AuditEvent do
let_it_be(:audit_event) { create(:project_audit_event) }
subject { audit_event }
describe 'validations' do describe 'validations' do
include_examples 'validates IP address' do include_examples 'validates IP address' do
let(:attribute) { :ip_address } let(:attribute) { :ip_address }
@ -13,6 +10,15 @@ RSpec.describe AuditEvent do
end end
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 describe '#as_json' do
context 'ip_address' do context 'ip_address' do
subject { build(:group_audit_event, ip_address: '192.168.1.1').as_json } 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 } let(:can_push) { true }
before_all do before_all do
project.add_guest(user) project.add_maintainer(user)
end end
context 'when this push_access_level is tied to a deploy key' do 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']) expect(user.errors.full_messages).to eq(['Username has already been taken'])
end end
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 end
it 'has a DB-level NOT NULL constraint on projects_limit' do it 'has a DB-level NOT NULL constraint on projects_limit' do
@ -2877,7 +2890,7 @@ RSpec.describe User do
end end
describe '#sanitize_attrs' do 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 it 'encodes HTML entities in the Skype attribute' do
expect { user.sanitize_attrs }.to change { user.skype }.to('test&amp;user') 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 it 'does not encode HTML entities in the name attribute' do
expect { user.sanitize_attrs }.not_to change { user.name } expect { user.sanitize_attrs }.not_to change { user.name }
end 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 end
describe '#starred?' do describe '#starred?' do
@ -5248,6 +5280,70 @@ RSpec.describe User do
end end
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 describe '#read_only_attribute?' do
context 'when synced attributes metadata is present' do context 'when synced attributes metadata is present' do
it 'delegates to synced_attributes_metadata' 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) } it { is_expected.not_to be_allowed(:access_api) }
end 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 context 'user with expired password' do
before do before do
current_user.update!(password_expires_at: 2.minutes.ago) current_user.update!(password_expires_at: 2.minutes.ago)
end end
it { is_expected.not_to be_allowed(:access_api) } 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 end
context 'when terms are enforced' do context 'when terms are enforced' do
@ -433,6 +449,14 @@ RSpec.describe GlobalPolicy do
end end
it { is_expected.not_to be_allowed(:access_git) } 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
end end
@ -517,6 +541,14 @@ RSpec.describe GlobalPolicy do
end end
it { is_expected.not_to be_allowed(:use_slash_commands) } 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
end end

View file

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

View file

@ -6,6 +6,9 @@ RSpec.describe 'GraphQL' do
include AfterNextHelpers include AfterNextHelpers
let(:query) { graphql_query_for('echo', text: 'Hello world') } 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 describe 'logging' do
shared_examples 'logging a graphql query' do shared_examples 'logging a graphql query' do
@ -70,6 +73,139 @@ RSpec.describe 'GraphQL' do
end end
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 context 'with invalid variables' do
it 'returns an error' do it 'returns an error' do
post_graphql(query, variables: "This is not JSON") post_graphql(query, variables: "This is not JSON")
@ -80,8 +216,6 @@ RSpec.describe 'GraphQL' do
end end
describe 'authentication', :allow_forgery_protection do describe 'authentication', :allow_forgery_protection do
let(:user) { create(:user) }
it 'allows access to public data without authentication' do it 'allows access to public data without authentication' do
post_graphql(query) post_graphql(query)
@ -109,11 +243,9 @@ RSpec.describe 'GraphQL' do
context 'with token authentication' do context 'with token authentication' do
let(:token) { create(:personal_access_token) } let(:token) { create(:personal_access_token) }
before do
stub_authentication_activity_metrics(debug: false)
end
it 'authenticates users with a PAT' do it 'authenticates users with a PAT' do
stub_authentication_activity_metrics(debug: false)
expect(authentication_metrics) expect(authentication_metrics)
.to increment(:user_authenticated_counter) .to increment(:user_authenticated_counter)
.and increment(:user_session_override_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") expect(graphql_data['echo']).to eq("\"#{token.user.username}\" says: Hello world")
end 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 context 'when the personal access token has no api scope' do
it 'does not log the user in' do it 'does not log the user in' do
token.update!(scopes: [:read_user]) token.update!(scopes: [:read_user])

View file

@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe API::ImportBitbucketServer do RSpec.describe API::ImportBitbucketServer do
let(:base_uri) { "https://test:7990" } let(:base_uri) { "https://test:7990" }
let(:user) { create(:user) } let(:user) { create(:user, bio: 'test') }
let(:token) { "asdasd12345" } let(:token) { "asdasd12345" }
let(:secret) { "sekrettt" } let(:secret) { "sekrettt" }
let(:project_key) { 'TES' } 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(:project, reload: true) { create(:project, :repository, namespace: user.namespace) }
let_it_be(:project2, reload: true) { create(:project, 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(: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 let_it_be(:project3, reload: true) do
create(:project, create(:project,
:private, :private,

View file

@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe API::Users do RSpec.describe API::Users do
let_it_be(:admin) { create(:admin) } 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(:key) { create(:key, user: user) }
let_it_be(:gpg_key) { create(:gpg_key, user: user) } let_it_be(:gpg_key) { create(:gpg_key, user: user) }
let_it_be(:email) { create(:email, user: user) } let_it_be(:email) { create(:email, user: user) }

View file

@ -36,16 +36,6 @@ RSpec.describe 'Git HTTP requests' do
end end
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 context "when user is blocked" do
let(:user) { create(:user, :blocked) } let(:user) { create(:user, :blocked) }
@ -68,6 +58,26 @@ RSpec.describe 'Git HTTP requests' do
end end
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 shared_examples 'pushes require Basic HTTP Authentication' do
context "when no credentials are provided" do context "when no credentials are provided" do
it "responds to uploads with status 401 Unauthorized (no project existence information leak)" 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 ') expect(response.header['WWW-Authenticate']).to start_with('Basic ')
end end
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 end
context "when authentication succeeds" do 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 'pulls require Basic HTTP Authentication'
it_behaves_like 'pushes 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 'when authenticated' do
it 'rejects downloads and uploads with 404 Not Found' 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 'pulls require Basic HTTP Authentication'
it_behaves_like 'pushes 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 'when authenticated' do
context 'and as a developer on the team' 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 'pulls require Basic HTTP Authentication'
it_behaves_like 'pushes require Basic HTTP Authentication' it_behaves_like 'pushes require Basic HTTP Authentication'
it_behaves_like 'operations are not allowed with expired password'
end end
context 'but the repo is enabled' do 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 'pulls require Basic HTTP Authentication'
it_behaves_like 'pushes require Basic HTTP Authentication' it_behaves_like 'pushes require Basic HTTP Authentication'
it_behaves_like 'operations are not allowed with expired password'
end end
end end
@ -508,6 +513,7 @@ RSpec.describe 'Git HTTP requests' do
it_behaves_like 'pulls require Basic HTTP Authentication' it_behaves_like 'pulls require Basic HTTP Authentication'
it_behaves_like 'pushes 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 context "when username and password are provided" do
let(:env) { { user: user.username, password: 'nope' } } 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 'pulls are allowed'
it_behaves_like 'pushes 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 end
end end

View file

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

View file

@ -68,12 +68,12 @@ RSpec.describe FeatureFlags::CreateService do
end end
it 'creates audit event' do it 'creates audit event' do
expected_message = 'Created feature flag <strong>feature_flag</strong> '\ expected_message = 'Created feature flag feature_flag '\
'with description <strong>"description"</strong>. '\ 'with description "description". '\
'Created rule <strong>*</strong> and set it as <strong>active</strong> '\ 'Created rule * and set it as active '\
'with strategies <strong>[{"name"=>"default", "parameters"=>{}}]</strong>. '\ 'with strategies [{"name"=&gt;"default", "parameters"=&gt;{}}]. '\
'Created rule <strong>production</strong> and set it as <strong>inactive</strong> '\ 'Created rule production and set it as inactive '\
'with strategies <strong>[{"name"=>"default", "parameters"=>{}}]</strong>.' 'with strategies [{"name"=&gt;"default", "parameters"=&gt;{}}].'
expect { subject }.to change { AuditEvent.count }.by(1) expect { subject }.to change { AuditEvent.count }.by(1)
expect(AuditEvent.last.details[:custom_message]).to eq(expected_message) 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 it 'creates audit log' do
expect { subject }.to change { AuditEvent.count }.by(1) 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 end
context 'when user is reporter' do 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 { subject }.to change { AuditEvent.count }.by(1)
expect(audit_event_message).to( expect(audit_event_message).to(
eq("Updated feature flag <strong>new_name</strong>. "\ eq("Updated feature flag new_name. "\
"Updated name from <strong>\"#{name_was}\"</strong> "\ "Updated name from \"#{name_was}\" "\
"to <strong>\"new_name\"</strong>.") "to \"new_name\".")
) )
end end
@ -94,8 +94,8 @@ RSpec.describe FeatureFlags::UpdateService do
it 'creates audit event with changed description' do it 'creates audit event with changed description' do
expect { subject }.to change { AuditEvent.count }.by(1) expect { subject }.to change { AuditEvent.count }.by(1)
expect(audit_event_message).to( expect(audit_event_message).to(
include("Updated description from <strong>\"\"</strong>"\ include("Updated description from \"\""\
" to <strong>\"new description\"</strong>.") " to \"new description\".")
) )
end end
end end
@ -110,7 +110,7 @@ RSpec.describe FeatureFlags::UpdateService do
it 'creates audit event about changing active state' do it 'creates audit event about changing active state' do
expect { subject }.to change { AuditEvent.count }.by(1) expect { subject }.to change { AuditEvent.count }.by(1)
expect(audit_event_message).to( expect(audit_event_message).to(
include('Updated active from <strong>"true"</strong> to <strong>"false"</strong>.') include('Updated active from "true" to "false".')
) )
end end
@ -132,8 +132,8 @@ RSpec.describe FeatureFlags::UpdateService do
it 'creates audit event about changing active state' do it 'creates audit event about changing active state' do
expect { subject }.to change { AuditEvent.count }.by(1) expect { subject }.to change { AuditEvent.count }.by(1)
expect(audit_event_message).to( expect(audit_event_message).to(
include("Updated rule <strong>*</strong> active state "\ include("Updated rule * active state "\
"from <strong>true</strong> to <strong>false</strong>.") "from true to false.")
) )
end end
end end
@ -149,8 +149,8 @@ RSpec.describe FeatureFlags::UpdateService do
it 'creates audit event with changed name' do it 'creates audit event with changed name' do
expect { subject }.to change { AuditEvent.count }.by(1) expect { subject }.to change { AuditEvent.count }.by(1)
expect(audit_event_message).to( expect(audit_event_message).to(
include("Updated rule <strong>staging</strong> environment scope "\ include("Updated rule staging environment scope "\
"from <strong>review</strong> to <strong>staging</strong>.") "from review to staging.")
) )
end end
@ -185,7 +185,7 @@ RSpec.describe FeatureFlags::UpdateService do
it 'creates audit event with deleted scope' do it 'creates audit event with deleted scope' do
expect { subject }.to change { AuditEvent.count }.by(1) 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 end
context 'when scope can not be deleted' do context 'when scope can not be deleted' do
@ -210,8 +210,8 @@ RSpec.describe FeatureFlags::UpdateService do
end end
it 'creates audit event with new scope' do it 'creates audit event with new scope' do
expected = 'Created rule <strong>review</strong> and set it as <strong>active</strong> '\ expected = 'Created rule review and set it as active '\
'with strategies <strong>[{"name"=>"default", "parameters"=>{}}]</strong>.' 'with strategies [{"name"=&gt;"default", "parameters"=&gt;{}}].'
subject subject
@ -260,7 +260,7 @@ RSpec.describe FeatureFlags::UpdateService do
end end
it 'creates an audit event' do 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 { subject }.to change { AuditEvent.count }.by(1)
expect(audit_event_message).to match(expected) expect(audit_event_message).to match(expected)

View file

@ -184,14 +184,6 @@ RSpec.describe Projects::ForkService do
end end
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 context "CI/CD settings" do
let(:to_project) { fork_project(@from_project, @to_user, using_service: true) } 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) expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
end 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
end end

View file

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

View file

@ -396,8 +396,13 @@ module GraphqlHelpers
post api('/', current_user, version: 'graphql'), params: { _json: queries }, headers: headers post api('/', current_user, version: 'graphql'), params: { _json: queries }, headers: headers
end end
def post_graphql(query, current_user: nil, variables: nil, headers: {}, token: {}) def get_multiplex(queries, current_user: nil, headers: {})
params = { query: query, variables: serialize_variables(variables) } 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 post api('/', current_user, version: 'graphql', **token), params: params, headers: headers
return unless graphql_errors return unless graphql_errors
@ -406,6 +411,18 @@ module GraphqlHelpers
expect(graphql_errors).not_to include(a_hash_including('message' => 'Internal server error')) expect(graphql_errors).not_to include(a_hash_including('message' => 'Internal server error'))
end 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: {}) def post_graphql_mutation(mutation, current_user: nil, token: {})
post_graphql(mutation.query, post_graphql(mutation.query,
current_user: current_user, current_user: current_user,