summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Release Tools Bot <delivery-team+release-tools@gitlab.com>2021-07-01 16:03:22 +0000
committerGitLab Release Tools Bot <delivery-team+release-tools@gitlab.com>2021-07-01 16:03:22 +0000
commit3aad3a0b6ffb1a0fe36db41f81e8bbd3728e5f80 (patch)
tree69cfc1a4f82d309ca58b361546824b44221b6585
parent76b84b42f64b8009cc181d5da0c656a8a521986d (diff)
parentbac4ee4a9e2bc845fd5c91240cccaa293cb4f847 (diff)
downloadgitlab-ce-3aad3a0b6ffb1a0fe36db41f81e8bbd3728e5f80.tar.gz
Merge remote-tracking branch 'dev/14-0-stable' into 14-0-stable
-rw-r--r--CHANGELOG.md23
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--Gemfile4
-rw-r--r--Gemfile.lock8
-rw-r--r--VERSION2
-rw-r--r--app/assets/javascripts/behaviors/markdown/copy_as_gfm.js3
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js24
-rw-r--r--app/assets/javascripts/releases/components/app_edit_new.vue9
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/not_diffable.vue2
-rw-r--r--app/controllers/graphql_controller.rb29
-rw-r--r--app/controllers/ide_controller.rb6
-rw-r--r--app/graphql/mutations/echo.rb33
-rw-r--r--app/graphql/types/mutation_type.rb1
-rw-r--r--app/models/audit_event.rb11
-rw-r--r--app/models/concerns/integrations/slack_mattermost_notifier.rb2
-rw-r--r--app/models/integrations/bamboo.rb1
-rw-r--r--app/models/integrations/base_issue_tracker.rb2
-rw-r--r--app/models/integrations/drone_ci.rb7
-rw-r--r--app/models/integrations/external_wiki.rb2
-rw-r--r--app/models/integrations/mock_ci.rb2
-rw-r--r--app/models/integrations/teamcity.rb5
-rw-r--r--app/models/integrations/unify_circuit.rb3
-rw-r--r--app/models/integrations/webex_teams.rb2
-rw-r--r--app/models/protected_branch/push_access_level.rb2
-rw-r--r--app/models/user.rb18
-rw-r--r--app/services/feature_flags/base_service.rb6
-rw-r--r--app/services/feature_flags/create_service.rb3
-rw-r--r--app/services/feature_flags/destroy_service.rb2
-rw-r--r--app/services/feature_flags/update_service.rb12
-rw-r--r--app/services/projects/fork_service.rb5
-rw-r--r--app/services/web_hook_service.rb3
-rw-r--r--app/views/projects/diffs/viewers/_not_diffable.html.haml2
-rw-r--r--doc/api/graphql/reference/index.md25
-rw-r--r--lib/gitlab/diff/file.rb11
-rw-r--r--lib/gitlab/diff/parser.rb2
-rw-r--r--lib/gitlab/git/diff.rb11
-rw-r--r--lib/gitlab/http.rb18
-rw-r--r--locale/gitlab.pot9
-rw-r--r--spec/controllers/graphql_controller_spec.rb6
-rw-r--r--spec/features/expand_collapse_diffs_spec.rb2
-rw-r--r--spec/features/projects/diffs/diff_show_spec.rb10
-rw-r--r--spec/features/snippets/notes_on_personal_snippets_spec.rb12
-rw-r--r--spec/frontend/behaviors/copy_as_gfm_spec.js48
-rw-r--r--spec/frontend/lib/utils/url_utility_spec.js34
-rw-r--r--spec/frontend/releases/components/app_edit_new_spec.js45
-rw-r--r--spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb2
-rw-r--r--spec/lib/gitlab/diff/file_spec.rb58
-rw-r--r--spec/lib/gitlab/diff/parser_spec.rb10
-rw-r--r--spec/lib/gitlab/http_spec.rb41
-rw-r--r--spec/models/audit_event_spec.rb12
-rw-r--r--spec/models/protected_branch/push_access_level_spec.rb2
-rw-r--r--spec/models/user_spec.rb34
-rw-r--r--spec/policies/global_policy_spec.rb8
-rw-r--r--spec/requests/api/graphql_spec.rb150
-rw-r--r--spec/requests/api/projects_spec.rb2
-rw-r--r--spec/requests/api/users_spec.rb2
-rw-r--r--spec/requests/ide_controller_spec.rb119
-rw-r--r--spec/services/feature_flags/create_service_spec.rb12
-rw-r--r--spec/services/feature_flags/destroy_service_spec.rb2
-rw-r--r--spec/services/feature_flags/update_service_spec.rb12
-rw-r--r--spec/services/projects/fork_service_spec.rb21
-rw-r--r--spec/support/helpers/graphql_helpers.rb21
62 files changed, 773 insertions, 204 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cf54b07c991..afba0f8b97f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,29 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 14.0.2 (2021-07-01)
+
+### Added (1 change)
+
+- [Added omniauth_user check when verifying user cap](gitlab-org/security/gitlab@68c5d856fbf83f5f5ade562ea84b6aa06db96c60) ([merge request](gitlab-org/security/gitlab!1501)) **GitLab Enterprise Edition**
+
+### Security (14 changes)
+
+- [Update rdoc to 6.3.1](gitlab-org/security/gitlab@341334cbb2d822f6aa057933934b819c34b87932) ([merge request](gitlab-org/security/gitlab!1533))
+- [Forbid GET requests with mutations](gitlab-org/security/gitlab@895c99b35efa6795fb050bfb4ef4574f3e32a373) ([merge request](gitlab-org/security/gitlab!1528))
+- [Prevent GraphQL API access by deactivated users](gitlab-org/security/gitlab@2dda4163dadc04b59ee3367990b72bee933adf9b) ([merge request](gitlab-org/security/gitlab!1525))
+- [Add sanitizing for name field](gitlab-org/security/gitlab@ecb5a598b87d670906df67ed4432426a375efa05) ([merge request](gitlab-org/security/gitlab!1499))
+- [Copy feature visibility settings to a fork](gitlab-org/security/gitlab@fcc87978b1c865c8bdcb3fc5d8dc221b7370192c) ([merge request](gitlab-org/security/gitlab!1522))
+- [Fix XSS on audit log for feature flag actions](gitlab-org/security/gitlab@94fc41d49e828a6457f1de31f2b239b087679c12) ([merge request](gitlab-org/security/gitlab!1521))
+- [Avoid disclosing project in web IDE](gitlab-org/security/gitlab@9de99878401713bc5f3a76ca85901dc3a9ca0cd8) ([merge request](gitlab-org/security/gitlab!1511))
+- [Sanitize input on pasteGFM](gitlab-org/security/gitlab@7bb97cfa11a11bb0725bc707dec73831e16fe177) ([merge request](gitlab-org/security/gitlab!1514))
+- [Fix merge request diff display issue with unsupported encoding](gitlab-org/security/gitlab@8c21afdce6c6214c14db1863df1aad80ed501377) ([merge request](gitlab-org/security/gitlab!1509))
+- [Fix deploy key fallback issue in protected branch](gitlab-org/security/gitlab@a24aa5412a8f1dad01359de6b2f0b66bb741f5d4) ([merge request](gitlab-org/security/gitlab!1508))
+- [Add total http read timeout](gitlab-org/security/gitlab@cf4e0aa0a3f668fb63de6721d062c3157fdd9f84) ([merge request](gitlab-org/security/gitlab!1507))
+- [Allow only same-origin URLs for Edit Release Cancel button](gitlab-org/security/gitlab@4b78e1e31f0a23b964953b1766d156e12a75115f) ([merge request](gitlab-org/security/gitlab!1506))
+- [Update Nokogiri to 1.11.4](gitlab-org/security/gitlab@c43001973ca1b684b4719f5559819179be2394da) ([merge request](gitlab-org/security/gitlab!1500))
+- [Add new username validation](gitlab-org/security/gitlab@c904a128f2c2262288d00f673294423316318f4d) ([merge request](gitlab-org/security/gitlab!1498))
+
## 14.0.1 (2021-06-24)
### Fixed (3 changes)
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 8d2e58b40f0..112969d1040 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-14.0.1 \ No newline at end of file
+14.0.2 \ No newline at end of file
diff --git a/Gemfile b/Gemfile
index c55cea60e31..72fd64b6f2e 100644
--- a/Gemfile
+++ b/Gemfile
@@ -157,7 +157,7 @@ gem 'github-markup', '~> 1.7.0', require: 'github/markup'
gem 'commonmarker', '~> 0.21'
gem 'kramdown', '~> 2.3.1'
gem 'RedCloth', '~> 4.3.2'
-gem 'rdoc', '~> 6.1.2'
+gem 'gitlab-rdoc', '~> 6.3.2', require: 'rdoc' # We need this fork until rdoc releases a new version. See https://gitlab.com/gitlab-org/gitlab/-/issues/334695
gem 'org-ruby', '~> 0.9.12'
gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1'
@@ -168,7 +168,7 @@ gem 'asciidoctor-kroki', '~> 0.4.0', require: false
gem 'rouge', '~> 3.26.0'
gem 'truncato', '~> 0.7.11'
gem 'bootstrap_form', '~> 4.2.0'
-gem 'nokogiri', '~> 1.11.1'
+gem 'nokogiri', '~> 1.11.4'
gem 'escape_utils', '~> 1.1'
# Calendar rendering
diff --git a/Gemfile.lock b/Gemfile.lock
index ef31d72a5dd..0555f933922 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -500,6 +500,7 @@ GEM
openid_connect (~> 1.2)
gitlab-pg_query (2.0.4)
google-protobuf (>= 3.17.1)
+ gitlab-rdoc (6.3.2)
gitlab-sidekiq-fetcher (0.5.6)
sidekiq (~> 5)
gitlab-styles (6.2.0)
@@ -799,7 +800,7 @@ GEM
netrc (0.11.0)
nio4r (2.5.4)
no_proxy_fix (0.1.2)
- nokogiri (1.11.3)
+ nokogiri (1.11.4)
mini_portile2 (~> 2.5.0)
racc (~> 1.4)
nokogumbo (2.0.2)
@@ -1024,7 +1025,6 @@ GEM
msgpack (>= 0.4.3)
optimist (>= 3.0.0)
rchardet (1.8.0)
- rdoc (6.1.2)
re2 (1.2.0)
recaptcha (4.13.1)
json
@@ -1497,6 +1497,7 @@ DEPENDENCIES
gitlab-net-dns (~> 0.9.1)
gitlab-omniauth-openid-connect (~> 0.4.0)
gitlab-pg_query (~> 2.0.4)
+ gitlab-rdoc (~> 6.3.2)
gitlab-sidekiq-fetcher (= 0.5.6)
gitlab-styles (~> 6.2.0)
gitlab_chronic_duration (~> 0.10.6.2)
@@ -1557,7 +1558,7 @@ DEPENDENCIES
net-ldap (~> 0.16.3)
net-ntp
net-ssh (~> 6.0)
- nokogiri (~> 1.11.1)
+ nokogiri (~> 1.11.4)
oauth2 (~> 1.4)
octokit (~> 4.15)
ohai (~> 16.10)
@@ -1605,7 +1606,6 @@ DEPENDENCIES
rainbow (~> 3.0)
rblineprof (~> 0.3.6)
rbtrace (~> 0.4)
- rdoc (~> 6.1.2)
re2 (~> 1.2.0)
recaptcha (~> 4.11)
redis (~> 4.0)
diff --git a/VERSION b/VERSION
index 8d2e58b40f0..112969d1040 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-14.0.1 \ No newline at end of file
+14.0.2 \ No newline at end of file
diff --git a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js
index 9a8af79210e..19ebab36481 100644
--- a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js
+++ b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import { sanitize } from '~/lib/dompurify';
import { getSelectedFragment, insertText } from '~/lib/utils/common_utils';
export class CopyAsGFM {
@@ -69,7 +70,7 @@ export class CopyAsGFM {
} else {
// Due to the async copy call we are not able to produce gfm so we transform the cached HTML
const div = document.createElement('div');
- div.innerHTML = gfmHtml;
+ div.innerHTML = sanitize(gfmHtml);
CopyAsGFM.nodeToGFM(div)
.then((transformedGfm) => {
CopyAsGFM.insertPastedText(e.target, text, transformedGfm);
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index 48abc072675..d68b41b7f7a 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -545,3 +545,27 @@ export function getURLOrigin(url) {
return null;
}
}
+
+/**
+ * Returns `true` if the given `url` resolves to the same origin the page is served
+ * from; otherwise, returns `false`.
+ *
+ * The `url` may be absolute or relative.
+ *
+ * @param {string} url The URL to check.
+ * @returns {boolean}
+ */
+export function isSameOriginUrl(url) {
+ if (typeof url !== 'string') {
+ return false;
+ }
+
+ const { origin } = window.location;
+
+ try {
+ return new URL(url, origin).origin === origin;
+ } catch {
+ // Invalid URLs cannot have the same origin
+ return false;
+ }
+}
diff --git a/app/assets/javascripts/releases/components/app_edit_new.vue b/app/assets/javascripts/releases/components/app_edit_new.vue
index aecd0d6371e..3774f97a060 100644
--- a/app/assets/javascripts/releases/components/app_edit_new.vue
+++ b/app/assets/javascripts/releases/components/app_edit_new.vue
@@ -2,6 +2,7 @@
import { GlButton, GlFormInput, GlFormGroup, GlSprintf } from '@gitlab/ui';
import { mapState, mapActions, mapGetters } from 'vuex';
import { getParameterByName } from '~/lib/utils/common_utils';
+import { isSameOriginUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import MilestoneCombobox from '~/milestones/components/milestone_combobox.vue';
import { BACK_URL_PARAM } from '~/releases/constants';
@@ -65,7 +66,13 @@ export default {
},
},
cancelPath() {
- return getParameterByName(BACK_URL_PARAM) || this.releasesPagePath;
+ const backUrl = getParameterByName(BACK_URL_PARAM);
+
+ if (isSameOriginUrl(backUrl)) {
+ return backUrl;
+ }
+
+ return this.releasesPagePath;
},
saveButtonLabel() {
return this.isExistingRelease ? __('Save changes') : __('Create release');
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/not_diffable.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/not_diffable.vue
index d4d3038f066..5a6b1c19027 100644
--- a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/not_diffable.vue
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/not_diffable.vue
@@ -1,5 +1,5 @@
<template>
<div class="nothing-here-block">
- {{ __('This diff was suppressed by a .gitattributes entry.') }}
+ {{ __("File suppressed by a .gitattributes entry or the file's encoding is unsupported.") }}
</div>
</template>
diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb
index 725d8b62c77..515fbd7b482 100644
--- a/app/controllers/graphql_controller.rb
+++ b/app/controllers/graphql_controller.rb
@@ -20,12 +20,16 @@ class GraphqlController < ApplicationController
# around in GraphiQL.
protect_from_forgery with: :null_session, only: :execute
- before_action :authorize_access_api!
+ # must come first: current_user is set up here
before_action(only: [:execute]) { authenticate_sessionless_user!(:api) }
+
+ before_action :authorize_access_api!
before_action :set_user_last_activity
before_action :track_vs_code_usage
before_action :disable_query_limiting
+ before_action :disallow_mutations_for_get
+
# Since we deactivate authentication from the main ApplicationController and
# defer it to :authorize_access_api!, we need to override the bypass session
# callback execution order here
@@ -62,6 +66,25 @@ class GraphqlController < ApplicationController
private
+ def disallow_mutations_for_get
+ return unless request.get? || request.head?
+ return unless any_mutating_query?
+
+ raise ::Gitlab::Graphql::Errors::ArgumentError, "Mutations are forbidden in #{request.request_method} requests"
+ end
+
+ def any_mutating_query?
+ if multiplex?
+ multiplex_queries.any? { |q| mutation?(q[:query], q[:operation_name]) }
+ else
+ mutation?(query)
+ end
+ end
+
+ def mutation?(query_string, operation_name = params[:operationName])
+ ::GraphQL::Query.new(GitlabSchema, query_string, operation_name: operation_name).mutation?
+ end
+
# Tests may mark some GraphQL queries as exempt from SQL query limits
def disable_query_limiting
return unless Gitlab::QueryLimiting.enabled_for_env?
@@ -130,7 +153,9 @@ class GraphqlController < ApplicationController
end
def authorize_access_api!
- access_denied!("API not accessible for user.") unless can?(current_user, :access_api)
+ return if can?(current_user, :access_api)
+
+ render_error('API not accessible for user', status: :forbidden)
end
# Overridden from the ApplicationController to make the response look like
diff --git a/app/controllers/ide_controller.rb b/app/controllers/ide_controller.rb
index 4c7a91ee602..44beceb4f48 100644
--- a/app/controllers/ide_controller.rb
+++ b/app/controllers/ide_controller.rb
@@ -7,6 +7,8 @@ class IdeController < ApplicationController
include StaticObjectExternalStorageCSP
include Gitlab::Utils::StrongMemoize
+ before_action :authorize_read_project!
+
before_action do
push_frontend_feature_flag(:build_service_proxy)
push_frontend_feature_flag(:schema_linting)
@@ -22,6 +24,10 @@ class IdeController < ApplicationController
private
+ def authorize_read_project!
+ render_404 unless can?(current_user, :read_project, project)
+ end
+
def define_index_vars
return unless project
diff --git a/app/graphql/mutations/echo.rb b/app/graphql/mutations/echo.rb
new file mode 100644
index 00000000000..61d39009ba4
--- /dev/null
+++ b/app/graphql/mutations/echo.rb
@@ -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
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index 6b1146f8f09..6d3327f9735 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -104,6 +104,7 @@ module Types
mount_mutation Mutations::Ci::RunnersRegistrationToken::Reset, feature_flag: :runner_graphql_query
mount_mutation Mutations::Namespace::PackageSettings::Update
mount_mutation Mutations::UserCallouts::Create
+ mount_mutation Mutations::Echo
end
end
diff --git a/app/models/audit_event.rb b/app/models/audit_event.rb
index aff7eef4622..11036b76fc1 100644
--- a/app/models/audit_event.rb
+++ b/app/models/audit_event.rb
@@ -32,6 +32,9 @@ class AuditEvent < ApplicationRecord
scope :by_author_id, -> (author_id) { where(author_id: author_id) }
after_initialize :initialize_details
+
+ before_validation :sanitize_message
+
# Note: The intention is to remove this once refactoring of AuditEvent
# has proceeded further.
#
@@ -83,6 +86,14 @@ class AuditEvent < ApplicationRecord
private
+ def sanitize_message
+ message = details[:custom_message]
+
+ return unless message
+
+ self.details = details.merge(custom_message: Sanitize.clean(message))
+ end
+
def default_author_value
::Gitlab::Audit::NullAuthor.for(author_id, (self[:author_name] || details[:author_name]))
end
diff --git a/app/models/concerns/integrations/slack_mattermost_notifier.rb b/app/models/concerns/integrations/slack_mattermost_notifier.rb
index a919fc840fd..cb6fafa8de0 100644
--- a/app/models/concerns/integrations/slack_mattermost_notifier.rb
+++ b/app/models/concerns/integrations/slack_mattermost_notifier.rb
@@ -17,7 +17,7 @@ module Integrations
class HTTPClient
def self.post(uri, params = {})
params.delete(:http_options) # these are internal to the client and we do not want them
- Gitlab::HTTP.post(uri, body: params)
+ Gitlab::HTTP.post(uri, body: params, use_read_total_timeout: true)
end
end
end
diff --git a/app/models/integrations/bamboo.rb b/app/models/integrations/bamboo.rb
index dbd7aedf4fe..fef2774c593 100644
--- a/app/models/integrations/bamboo.rb
+++ b/app/models/integrations/bamboo.rb
@@ -173,6 +173,7 @@ module Integrations
query_params[:os_authType] = 'basic'
params[:basic_auth] = basic_auth
+ params[:use_read_total_timeout] = true
params
end
diff --git a/app/models/integrations/base_issue_tracker.rb b/app/models/integrations/base_issue_tracker.rb
index 6c24f762cd5..3fd67205e92 100644
--- a/app/models/integrations/base_issue_tracker.rb
+++ b/app/models/integrations/base_issue_tracker.rb
@@ -107,7 +107,7 @@ module Integrations
result = false
begin
- response = Gitlab::HTTP.head(self.project_url, verify: true)
+ response = Gitlab::HTTP.head(self.project_url, verify: true, use_read_total_timeout: true)
if response
message = "#{self.type} received response #{response.code} when attempting to connect to #{self.project_url}"
diff --git a/app/models/integrations/drone_ci.rb b/app/models/integrations/drone_ci.rb
index 096f7093b8c..0f021356815 100644
--- a/app/models/integrations/drone_ci.rb
+++ b/app/models/integrations/drone_ci.rb
@@ -51,9 +51,12 @@ module Integrations
end
def calculate_reactive_cache(sha, ref)
- response = Gitlab::HTTP.try_get(commit_status_path(sha, ref),
+ response = Gitlab::HTTP.try_get(
+ commit_status_path(sha, ref),
verify: enable_ssl_verification,
- extra_log_info: { project_id: project_id })
+ extra_log_info: { project_id: project_id },
+ use_read_total_timeout: true
+ )
status =
if response && response.code == 200 && response['status']
diff --git a/app/models/integrations/external_wiki.rb b/app/models/integrations/external_wiki.rb
index fec435443fa..2a8d598117b 100644
--- a/app/models/integrations/external_wiki.rb
+++ b/app/models/integrations/external_wiki.rb
@@ -39,7 +39,7 @@ module Integrations
end
def execute(_data)
- response = Gitlab::HTTP.get(properties['external_wiki_url'], verify: true)
+ response = Gitlab::HTTP.get(properties['external_wiki_url'], verify: true, use_read_total_timeout: true)
response.body if response.code == 200
rescue StandardError
nil
diff --git a/app/models/integrations/mock_ci.rb b/app/models/integrations/mock_ci.rb
index d31f6381767..a0eae9e4abf 100644
--- a/app/models/integrations/mock_ci.rb
+++ b/app/models/integrations/mock_ci.rb
@@ -55,7 +55,7 @@ module Integrations
# # => 'running'
#
def commit_status(sha, ref)
- response = Gitlab::HTTP.get(commit_status_path(sha), verify: false)
+ response = Gitlab::HTTP.get(commit_status_path(sha), verify: false, use_read_total_timeout: true)
read_commit_status(response)
rescue Errno::ECONNREFUSED
:error
diff --git a/app/models/integrations/teamcity.rb b/app/models/integrations/teamcity.rb
index 8284d5963ae..3f14c5d82b3 100644
--- a/app/models/integrations/teamcity.rb
+++ b/app/models/integrations/teamcity.rb
@@ -170,7 +170,7 @@ module Integrations
end
def get_path(path)
- Gitlab::HTTP.try_get(build_url(path), verify: false, basic_auth: basic_auth, extra_log_info: { project_id: project_id })
+ Gitlab::HTTP.try_get(build_url(path), verify: false, basic_auth: basic_auth, extra_log_info: { project_id: project_id }, use_read_total_timeout: true)
end
def post_to_build_queue(data, branch)
@@ -180,7 +180,8 @@ module Integrations
"<buildType id=#{build_type.encode(xml: :attr)}/>"\
'</build>',
headers: { 'Content-type' => 'application/xml' },
- basic_auth: basic_auth
+ basic_auth: basic_auth,
+ use_read_total_timeout: true
)
end
diff --git a/app/models/integrations/unify_circuit.rb b/app/models/integrations/unify_circuit.rb
index 03363c7c8b0..834222834e9 100644
--- a/app/models/integrations/unify_circuit.rb
+++ b/app/models/integrations/unify_circuit.rb
@@ -49,7 +49,8 @@ module Integrations
response = Gitlab::HTTP.post(webhook, body: {
subject: message.project_name,
text: message.summary,
- markdown: true
+ markdown: true,
+ use_read_total_timeout: true
}.to_json)
response if response.success?
diff --git a/app/models/integrations/webex_teams.rb b/app/models/integrations/webex_teams.rb
index 3f420331035..6fd82a32035 100644
--- a/app/models/integrations/webex_teams.rb
+++ b/app/models/integrations/webex_teams.rb
@@ -44,7 +44,7 @@ module Integrations
def notify(message, opts)
header = { 'Content-Type' => 'application/json' }
- response = Gitlab::HTTP.post(webhook, headers: header, body: { markdown: message.summary }.to_json)
+ response = Gitlab::HTTP.post(webhook, headers: header, body: { markdown: message.summary }.to_json, use_read_total_timeout: true)
response if response.success?
end
diff --git a/app/models/protected_branch/push_access_level.rb b/app/models/protected_branch/push_access_level.rb
index ea51dca8a42..5248834a2f2 100644
--- a/app/models/protected_branch/push_access_level.rb
+++ b/app/models/protected_branch/push_access_level.rb
@@ -20,7 +20,7 @@ class ProtectedBranch::PushAccessLevel < ApplicationRecord
def check_access(user)
if user && deploy_key.present?
- return true if user.can?(:read_project, project) && enabled_deploy_key_for_user?(deploy_key, user)
+ return user.can?(:read_project, project) && enabled_deploy_key_for_user?(deploy_key, user)
end
super
diff --git a/app/models/user.rb b/app/models/user.rb
index 5fbd6271589..52bf9149ee2 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -236,6 +236,7 @@ class User < ApplicationRecord
validate :owns_commit_email, if: :commit_email_changed?
validate :signup_domain_valid?, on: :create, if: ->(user) { !user.created_by_id }
validate :check_email_restrictions, on: :create, if: ->(user) { !user.created_by_id }
+ validate :check_username_format, if: :username_changed?
validates :theme_id, allow_nil: true, inclusion: { in: Gitlab::Themes.valid_ids,
message: _("%{placeholder} is not a valid theme") % { placeholder: '%{value}' } }
@@ -1257,12 +1258,23 @@ class User < ApplicationRecord
end
def sanitize_attrs
+ sanitize_links
+ sanitize_name
+ end
+
+ def sanitize_links
%i[skype linkedin twitter].each do |attr|
value = self[attr]
self[attr] = Sanitize.clean(value) if value.present?
end
end
+ def sanitize_name
+ return unless self.name
+
+ self.name = self.name.gsub(%r{</?[^>]*>}, '')
+ end
+
def set_notification_email
if notification_email.blank? || all_emails.exclude?(notification_email)
self.notification_email = email
@@ -2082,6 +2094,12 @@ class User < ApplicationRecord
end
end
+ def check_username_format
+ return if username.blank? || Mime::EXTENSION_LOOKUP.keys.none? { |type| username.end_with?(type) }
+
+ errors.add(:username, _('ending with MIME type format is not allowed.'))
+ end
+
def groups_with_developer_maintainer_project_access
project_creation_levels = [::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS]
diff --git a/app/services/feature_flags/base_service.rb b/app/services/feature_flags/base_service.rb
index f48f95e2550..d041703803b 100644
--- a/app/services/feature_flags/base_service.rb
+++ b/app/services/feature_flags/base_service.rb
@@ -49,9 +49,9 @@ module FeatureFlags
end
def created_scope_message(scope)
- "Created rule <strong>#{scope.environment_scope}</strong> "\
- "and set it as <strong>#{scope.active ? "active" : "inactive"}</strong> "\
- "with strategies <strong>#{scope.strategies}</strong>."
+ "Created rule #{scope.environment_scope} "\
+ "and set it as #{scope.active ? "active" : "inactive"} "\
+ "with strategies #{scope.strategies}."
end
def feature_flag_by_name
diff --git a/app/services/feature_flags/create_service.rb b/app/services/feature_flags/create_service.rb
index de3a55d10fc..5c87af561d5 100644
--- a/app/services/feature_flags/create_service.rb
+++ b/app/services/feature_flags/create_service.rb
@@ -22,8 +22,7 @@ module FeatureFlags
private
def audit_message(feature_flag)
- message_parts = ["Created feature flag <strong>#{feature_flag.name}</strong>",
- "with description <strong>\"#{feature_flag.description}\"</strong>."]
+ message_parts = ["Created feature flag #{feature_flag.name} with description \"#{feature_flag.description}\"."]
message_parts += feature_flag.scopes.map do |scope|
created_scope_message(scope)
diff --git a/app/services/feature_flags/destroy_service.rb b/app/services/feature_flags/destroy_service.rb
index c77e3e03ec3..b131a349fc7 100644
--- a/app/services/feature_flags/destroy_service.rb
+++ b/app/services/feature_flags/destroy_service.rb
@@ -23,7 +23,7 @@ module FeatureFlags
end
def audit_message(feature_flag)
- "Deleted feature flag <strong>#{feature_flag.name}</strong>."
+ "Deleted feature flag #{feature_flag.name}."
end
def can_destroy?(feature_flag)
diff --git a/app/services/feature_flags/update_service.rb b/app/services/feature_flags/update_service.rb
index d956d4b3357..f5ab6f4005b 100644
--- a/app/services/feature_flags/update_service.rb
+++ b/app/services/feature_flags/update_service.rb
@@ -45,14 +45,14 @@ module FeatureFlags
return if changes.empty?
- "Updated feature flag <strong>#{feature_flag.name}</strong>. " + changes.join(" ")
+ "Updated feature flag #{feature_flag.name}. " + changes.join(" ")
end
def changed_attributes_messages(feature_flag)
feature_flag.changes.slice(*AUDITABLE_ATTRIBUTES).map do |attribute_name, changes|
"Updated #{attribute_name} "\
- "from <strong>\"#{changes.first}\"</strong> to "\
- "<strong>\"#{changes.second}\"</strong>."
+ "from \"#{changes.first}\" to "\
+ "\"#{changes.second}\"."
end
end
@@ -69,17 +69,17 @@ module FeatureFlags
end
def deleted_scope_message(scope)
- "Deleted rule <strong>#{scope.environment_scope}</strong>."
+ "Deleted rule #{scope.environment_scope}."
end
def updated_scope_message(scope)
changes = scope.changes.slice(*AUDITABLE_SCOPE_ATTRIBUTES_HUMAN_NAMES.keys)
return if changes.empty?
- message = "Updated rule <strong>#{scope.environment_scope}</strong> "
+ message = "Updated rule #{scope.environment_scope} "
message += changes.map do |attribute_name, change|
name = AUDITABLE_SCOPE_ATTRIBUTES_HUMAN_NAMES[attribute_name]
- "#{name} from <strong>#{change.first}</strong> to <strong>#{change.second}</strong>"
+ "#{name} from #{change.first} to #{change.second}"
end.join(' ')
message + '.'
diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb
index fd9b64a4ee0..3cee1b5975a 100644
--- a/app/services/projects/fork_service.rb
+++ b/app/services/projects/fork_service.rb
@@ -34,8 +34,9 @@ module Projects
new_project = CreateService.new(current_user, new_fork_params).execute
return new_project unless new_project.persisted?
- builds_access_level = @project.project_feature.builds_access_level
- new_project.project_feature.update(builds_access_level: builds_access_level)
+ new_project.project_feature.update!(
+ @project.project_feature.slice(ProjectFeature::FEATURES.map { |f| "#{f}_access_level" })
+ )
new_project
end
diff --git a/app/services/web_hook_service.rb b/app/services/web_hook_service.rb
index 77d2139b3d1..1d5b38575bb 100644
--- a/app/services/web_hook_service.rb
+++ b/app/services/web_hook_service.rb
@@ -42,6 +42,7 @@ class WebHookService
@uniqueness_token = uniqueness_token
@request_options = {
timeout: Gitlab.config.gitlab.webhook_timeout,
+ use_read_total_timeout: true,
allow_local_requests: hook.allow_local_requests?
}
end
@@ -68,7 +69,7 @@ class WebHookService
{
status: :success,
http_status: response.code,
- message: response.to_s
+ message: response.body
}
rescue *Gitlab::HTTP::HTTP_ERRORS,
Gitlab::Json::LimitedEncoder::LimitExceeded, URI::InvalidURIError => e
diff --git a/app/views/projects/diffs/viewers/_not_diffable.html.haml b/app/views/projects/diffs/viewers/_not_diffable.html.haml
index 7c55e272f56..63034331f6a 100644
--- a/app/views/projects/diffs/viewers/_not_diffable.html.haml
+++ b/app/views/projects/diffs/viewers/_not_diffable.html.haml
@@ -1,2 +1,2 @@
.nothing-here-block
- = _("This diff was suppressed by a .gitattributes entry.")
+ = _("File suppressed by a .gitattributes entry or the file's encoding is unsupported.")
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index e84769fa568..ffdf0ea5fd3 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -1895,6 +1895,31 @@ Input type: `DiscussionToggleResolveInput`
| <a id="mutationdiscussiontoggleresolvediscussion"></a>`discussion` | [`Discussion`](#discussion) | The discussion after mutation. |
| <a id="mutationdiscussiontoggleresolveerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+### `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.enableDevopsAdoptionNamespace`
**BETA** This endpoint is subject to change without notice.
diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb
index dcd4bbdabf5..35581952f4a 100644
--- a/lib/gitlab/diff/file.rb
+++ b/lib/gitlab/diff/file.rb
@@ -250,7 +250,7 @@ module Gitlab
end
def diffable?
- repository.attributes(file_path).fetch('diff') { true }
+ diffable_by_attribute? && !text_with_binary_notice?
end
def binary_in_repo?
@@ -366,6 +366,15 @@ module Gitlab
private
+ def diffable_by_attribute?
+ repository.attributes(file_path).fetch('diff') { true }
+ end
+
+ # NOTE: Files with unsupported encodings (e.g. UTF-16) are treated as binary by git, but they are recognized as text files during encoding detection. These files have `Binary files a/filename and b/filename differ' as their raw diff content which cannot be used. We need to handle this special case and avoid displaying incorrect diff.
+ def text_with_binary_notice?
+ text? && has_binary_notice?
+ end
+
def fetch_blob(sha, path)
return unless sha
diff --git a/lib/gitlab/diff/parser.rb b/lib/gitlab/diff/parser.rb
index 4a47e4b80b6..adb711ca89f 100644
--- a/lib/gitlab/diff/parser.rb
+++ b/lib/gitlab/diff/parser.rb
@@ -6,7 +6,7 @@ module Gitlab
include Enumerable
def parse(lines, diff_file: nil)
- return [] if lines.blank?
+ return [] if lines.blank? || Git::Diff.has_binary_notice?(lines.first)
@lines = lines
line_obj_index = 0
diff --git a/lib/gitlab/git/diff.rb b/lib/gitlab/git/diff.rb
index 53df0b7b389..8325eadce2f 100644
--- a/lib/gitlab/git/diff.rb
+++ b/lib/gitlab/git/diff.rb
@@ -33,6 +33,8 @@ module Gitlab
SERIALIZE_KEYS = %i(diff new_path old_path a_mode b_mode new_file renamed_file deleted_file too_large).freeze
+ BINARY_NOTICE_PATTERN = %r(Binary files a\/(.*) and b\/(.*) differ).freeze
+
class << self
def between(repo, head, base, options = {}, *paths)
straight = options.delete(:straight) || false
@@ -131,8 +133,13 @@ module Gitlab
def patch_hard_limit_bytes
Gitlab::CurrentSettings.diff_max_patch_bytes
end
- end
+ def has_binary_notice?(text)
+ return false unless text.present?
+
+ text.start_with?(BINARY_NOTICE_PATTERN)
+ end
+ end
def initialize(raw_diff, expanded: true)
@expanded = expanded
@@ -215,7 +222,7 @@ module Gitlab
end
def has_binary_notice?
- @diff.start_with?('Binary')
+ self.class.has_binary_notice?(@diff)
end
private
diff --git a/lib/gitlab/http.rb b/lib/gitlab/http.rb
index be87dcc0ff9..7e45cd216f5 100644
--- a/lib/gitlab/http.rb
+++ b/lib/gitlab/http.rb
@@ -8,9 +8,10 @@ module Gitlab
class HTTP
BlockedUrlError = Class.new(StandardError)
RedirectionTooDeep = Class.new(StandardError)
+ ReadTotalTimeout = Class.new(Net::ReadTimeout)
HTTP_TIMEOUT_ERRORS = [
- Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout
+ Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout, Gitlab::HTTP::ReadTotalTimeout
].freeze
HTTP_ERRORS = HTTP_TIMEOUT_ERRORS + [
SocketError, OpenSSL::SSL::SSLError, OpenSSL::OpenSSLError,
@@ -23,6 +24,7 @@ module Gitlab
read_timeout: 20,
write_timeout: 30
}.freeze
+ DEFAULT_READ_TOTAL_TIMEOUT = 20.seconds
include HTTParty # rubocop:disable Gitlab/HTTParty
@@ -41,7 +43,19 @@ module Gitlab
options
end
- httparty_perform_request(http_method, path, options_with_timeouts, &block)
+ unless options.has_key?(:use_read_total_timeout)
+ return httparty_perform_request(http_method, path, options_with_timeouts, &block)
+ end
+
+ start_time = Gitlab::Metrics::System.monotonic_time
+ read_total_timeout = options.fetch(:timeout, DEFAULT_READ_TOTAL_TIMEOUT)
+
+ httparty_perform_request(http_method, path, options_with_timeouts) do |fragment|
+ elapsed = Gitlab::Metrics::System.monotonic_time - start_time
+ raise ReadTotalTimeout, "Request timed out after #{elapsed} seconds" if elapsed > read_total_timeout
+
+ block.call fragment if block
+ end
rescue HTTParty::RedirectionTooDeep
raise RedirectionTooDeep
rescue *HTTP_ERRORS => e
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index f34bea613e8..feb3d972d2a 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -14032,6 +14032,9 @@ msgstr ""
msgid "File renamed with no changes."
msgstr ""
+msgid "File suppressed by a .gitattributes entry or the file's encoding is unsupported."
+msgstr ""
+
msgid "File synchronization concurrency limit"
msgstr ""
@@ -33273,9 +33276,6 @@ msgstr ""
msgid "This diff is collapsed."
msgstr ""
-msgid "This diff was suppressed by a .gitattributes entry."
-msgstr ""
-
msgid "This directory"
msgstr ""
@@ -38542,6 +38542,9 @@ msgstr ""
msgid "encrypted: needs to be a :required, :optional or :migrating!"
msgstr ""
+msgid "ending with MIME type format is not allowed."
+msgstr ""
+
msgid "entries cannot be larger than 255 characters"
msgstr ""
diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb
index f2d86b1b166..aed97a01a72 100644
--- a/spec/controllers/graphql_controller_spec.rb
+++ b/spec/controllers/graphql_controller_spec.rb
@@ -44,7 +44,7 @@ RSpec.describe GraphqlController do
expect(response).to have_gitlab_http_status(:ok)
end
- it 'returns access denied template when user cannot access API' do
+ it 'returns forbidden when user cannot access API' do
# User cannot access API in a couple of cases
# * When user is internal(like ghost users)
# * When user is blocked
@@ -54,7 +54,9 @@ RSpec.describe GraphqlController do
post :execute
expect(response).to have_gitlab_http_status(:forbidden)
- expect(response).to render_template('errors/access_denied')
+ expect(json_response).to include(
+ 'errors' => include(a_hash_including('message' => /API not accessible/))
+ )
end
it 'updates the users last_activity_on field' do
diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb
index cbd1ae628d1..add4af2bcdb 100644
--- a/spec/features/expand_collapse_diffs_spec.rb
+++ b/spec/features/expand_collapse_diffs_spec.rb
@@ -253,7 +253,7 @@ RSpec.describe 'Expand and collapse diffs', :js do
click_link('Expand all')
# Wait for elements to appear to ensure full page reload
- expect(page).to have_content('This diff was suppressed by a .gitattributes entry')
+ expect(page).to have_content("File suppressed by a .gitattributes entry or the file's encoding is unsupported.")
expect(page).to have_content('This source diff could not be displayed because it is too large.')
expect(page).to have_content('too_large_image.jpg')
find('.note-textarea')
diff --git a/spec/features/projects/diffs/diff_show_spec.rb b/spec/features/projects/diffs/diff_show_spec.rb
index e47f36c4b7a..56506ada3ce 100644
--- a/spec/features/projects/diffs/diff_show_spec.rb
+++ b/spec/features/projects/diffs/diff_show_spec.rb
@@ -174,4 +174,14 @@ RSpec.describe 'Diff file viewer', :js, :with_clean_rails_cache do
end
end
end
+
+ context 'when the the encoding of the file is unsupported' do
+ before do
+ visit_commit('f05a98786e4274708e1fa118c7ad3a29d1d1b9a3')
+ end
+
+ it 'shows it is not diffable' do
+ expect(page).to have_content("File suppressed by a .gitattributes entry or the file's encoding is unsupported.")
+ end
+ end
end
diff --git a/spec/features/snippets/notes_on_personal_snippets_spec.rb b/spec/features/snippets/notes_on_personal_snippets_spec.rb
index 47dad9bd88e..e03f71c5352 100644
--- a/spec/features/snippets/notes_on_personal_snippets_spec.rb
+++ b/spec/features/snippets/notes_on_personal_snippets_spec.rb
@@ -65,18 +65,6 @@ RSpec.describe 'Comments on personal snippets', :js do
expect(page).to have_content(user_name)
end
end
-
- context 'when the author name contains HTML' do
- let(:user_name) { '<h1><a href="https://bad.link/malicious.exe" class="evil">Fake Content<img class="fake-icon" src="image.png"></a></h1>' }
-
- it 'renders the name as plain text' do
- visit snippet_path(snippet)
-
- content = find("#note_#{snippet_notes[0].id} .note-header-author-name").text
-
- expect(content).to eq user_name
- end
- end
end
context 'when submitting a note' do
diff --git a/spec/frontend/behaviors/copy_as_gfm_spec.js b/spec/frontend/behaviors/copy_as_gfm_spec.js
index acff990e84a..557b609f5f9 100644
--- a/spec/frontend/behaviors/copy_as_gfm_spec.js
+++ b/spec/frontend/behaviors/copy_as_gfm_spec.js
@@ -1,50 +1,54 @@
import initCopyAsGFM, { CopyAsGFM } from '~/behaviors/markdown/copy_as_gfm';
-import * as commonUtils from '~/lib/utils/common_utils';
describe('CopyAsGFM', () => {
describe('CopyAsGFM.pasteGFM', () => {
- function callPasteGFM() {
+ let target;
+
+ beforeEach(() => {
+ target = document.createElement('input');
+ target.value = 'This is code: ';
+ });
+
+ // When GFM code is copied, we put the regular plain text
+ // on the clipboard as `text/plain`, and the GFM as `text/x-gfm`.
+ // This emulates the behavior of `getData` with that data.
+ function callPasteGFM(data = { 'text/plain': 'code', 'text/x-gfm': '`code`' }) {
const e = {
originalEvent: {
clipboardData: {
getData(mimeType) {
- // When GFM code is copied, we put the regular plain text
- // on the clipboard as `text/plain`, and the GFM as `text/x-gfm`.
- // This emulates the behavior of `getData` with that data.
- if (mimeType === 'text/plain') {
- return 'code';
- }
- if (mimeType === 'text/x-gfm') {
- return '`code`';
- }
- return null;
+ return data[mimeType] || null;
},
},
},
preventDefault() {},
+ target,
};
CopyAsGFM.pasteGFM(e);
}
it('wraps pasted code when not already in code tags', () => {
- jest.spyOn(commonUtils, 'insertText').mockImplementation((el, textFunc) => {
- const insertedText = textFunc('This is code: ', '');
+ callPasteGFM();
- expect(insertedText).toEqual('`code`');
- });
+ expect(target.value).toBe('This is code: `code`');
+ });
+
+ it('does not wrap pasted code when already in code tags', () => {
+ target.value = 'This is code: `';
callPasteGFM();
+
+ expect(target.value).toBe('This is code: `code');
});
- it('does not wrap pasted code when already in code tags', () => {
- jest.spyOn(commonUtils, 'insertText').mockImplementation((el, textFunc) => {
- const insertedText = textFunc('This is code: `', '`');
+ it('does not allow xss in x-gfm-html', () => {
+ const testEl = document.createElement('div');
+ jest.spyOn(document, 'createElement').mockReturnValueOnce(testEl);
- expect(insertedText).toEqual('code');
- });
+ callPasteGFM({ 'text/plain': 'code', 'text/x-gfm-html': 'code<img/src/onerror=alert(1)>' });
- callPasteGFM();
+ expect(testEl.innerHTML).toBe('code<img src="">');
});
});
diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js
index 305d3de3c53..31c78681994 100644
--- a/spec/frontend/lib/utils/url_utility_spec.js
+++ b/spec/frontend/lib/utils/url_utility_spec.js
@@ -1,3 +1,4 @@
+import { TEST_HOST } from 'helpers/test_constants';
import * as urlUtils from '~/lib/utils/url_utility';
const shas = {
@@ -923,4 +924,37 @@ describe('URL utility', () => {
expect(urlUtils.encodeSaferUrl(input)).toBe(input);
});
});
+
+ describe('isSameOriginUrl', () => {
+ // eslint-disable-next-line no-script-url
+ const javascriptUrl = 'javascript:alert(1)';
+
+ beforeEach(() => {
+ setWindowLocation({ origin: TEST_HOST });
+ });
+
+ it.each`
+ url | expected
+ ${TEST_HOST} | ${true}
+ ${`${TEST_HOST}/a/path`} | ${true}
+ ${'//test.host/no-protocol'} | ${true}
+ ${'/a/root/relative/path'} | ${true}
+ ${'a/relative/path'} | ${true}
+ ${'#hash'} | ${true}
+ ${'?param=foo'} | ${true}
+ ${''} | ${true}
+ ${'../../../'} | ${true}
+ ${`${TEST_HOST}:8080/wrong-port`} | ${false}
+ ${'ws://test.host/wrong-protocol'} | ${false}
+ ${'http://phishing.test'} | ${false}
+ ${'//phishing.test'} | ${false}
+ ${'//invalid:url'} | ${false}
+ ${javascriptUrl} | ${false}
+ ${'data:,Hello%2C%20World%21'} | ${false}
+ ${null} | ${false}
+ ${undefined} | ${false}
+ `('returns $expected given $url', ({ url, expected }) => {
+ expect(urlUtils.isSameOriginUrl(url)).toBe(expected);
+ });
+ });
});
diff --git a/spec/frontend/releases/components/app_edit_new_spec.js b/spec/frontend/releases/components/app_edit_new_spec.js
index 65ed6d6166f..748b48dacaa 100644
--- a/spec/frontend/releases/components/app_edit_new_spec.js
+++ b/spec/frontend/releases/components/app_edit_new_spec.js
@@ -4,6 +4,7 @@ import MockAdapter from 'axios-mock-adapter';
import { merge } from 'lodash';
import Vuex from 'vuex';
import { getJSONFixture } from 'helpers/fixtures';
+import { TEST_HOST } from 'helpers/test_constants';
import * as commonUtils from '~/lib/utils/common_utils';
import ReleaseEditNewApp from '~/releases/components/app_edit_new.vue';
import AssetLinksForm from '~/releases/components/asset_links_form.vue';
@@ -11,6 +12,7 @@ import { BACK_URL_PARAM } from '~/releases/constants';
const originalRelease = getJSONFixture('api/releases/release.json');
const originalMilestones = originalRelease.milestones;
+const releasesPagePath = 'path/to/releases/page';
describe('Release edit/new component', () => {
let wrapper;
@@ -24,7 +26,7 @@ describe('Release edit/new component', () => {
state = {
release,
markdownDocsPath: 'path/to/markdown/docs',
- releasesPagePath: 'path/to/releases/page',
+ releasesPagePath,
projectId: '8',
groupId: '42',
groupMilestonesAvailable: true,
@@ -75,6 +77,8 @@ describe('Release edit/new component', () => {
};
beforeEach(() => {
+ global.jsdom.reconfigure({ url: TEST_HOST });
+
mock = new MockAdapter(axios);
gon.api_version = 'v4';
@@ -146,22 +150,33 @@ describe('Release edit/new component', () => {
});
});
- describe(`when the URL contains a "${BACK_URL_PARAM}" parameter`, () => {
- const backUrl = 'https://example.gitlab.com/back/url';
-
- beforeEach(async () => {
- commonUtils.getParameterByName = jest
- .fn()
- .mockImplementation((paramToGet) => ({ [BACK_URL_PARAM]: backUrl }[paramToGet]));
+ // eslint-disable-next-line no-script-url
+ const xssBackUrl = 'javascript:alert(1)';
+ describe.each`
+ backUrl | expectedHref
+ ${`${TEST_HOST}/back/url`} | ${`${TEST_HOST}/back/url`}
+ ${`/back/url?page=2`} | ${`/back/url?page=2`}
+ ${`back/url?page=3`} | ${`back/url?page=3`}
+ ${'http://phishing.test/back/url'} | ${releasesPagePath}
+ ${'//phishing.test/back/url'} | ${releasesPagePath}
+ ${xssBackUrl} | ${releasesPagePath}
+ `(
+ `when the URL contains a "${BACK_URL_PARAM}=$backUrl" parameter`,
+ ({ backUrl, expectedHref }) => {
+ beforeEach(async () => {
+ global.jsdom.reconfigure({
+ url: `${TEST_HOST}?${BACK_URL_PARAM}=${encodeURIComponent(backUrl)}`,
+ });
- await factory();
- });
+ await factory();
+ });
- it('renders a "Cancel" button with an href pointing to the main Releases page', () => {
- const cancelButton = wrapper.find('.js-cancel-button');
- expect(cancelButton.attributes().href).toBe(backUrl);
- });
- });
+ it(`renders a "Cancel" button with an href pointing to ${expectedHref}`, () => {
+ const cancelButton = wrapper.find('.js-cancel-button');
+ expect(cancelButton.attributes().href).toBe(expectedHref);
+ });
+ },
+ );
describe('when creating a new release', () => {
beforeEach(async () => {
diff --git a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb
index 03a9b9bd21e..d401c42fed7 100644
--- a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb
+++ b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe Gitlab::Diff::FileCollection::MergeRequestDiff do
it 'does not highlight files marked as undiffable in .gitattributes' do
allow_next_instance_of(Gitlab::Diff::File) do |instance|
- allow(instance).to receive(:diffable?).and_return(false)
+ allow(instance).to receive(:diffable_by_attribute?).and_return(false)
end
expect_next_instance_of(Gitlab::Diff::File) do |instance|
diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb
index 78be89c449b..1800d2d6b60 100644
--- a/spec/lib/gitlab/diff/file_spec.rb
+++ b/spec/lib/gitlab/diff/file_spec.rb
@@ -186,26 +186,46 @@ RSpec.describe Gitlab::Diff::File do
end
describe '#diffable?' do
- let(:commit) { project.commit('1a0b36b3cdad1d2ee32457c102a8c0b7056fa863') }
- let(:diffs) { commit.diffs }
+ context 'when attributes exist' do
+ let(:commit) { project.commit('1a0b36b3cdad1d2ee32457c102a8c0b7056fa863') }
+ let(:diffs) { commit.diffs }
- before do
- info_dir_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- File.join(project.repository.path_to_repo, 'info')
+ before do
+ info_dir_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ File.join(project.repository.path_to_repo, 'info')
+ end
+
+ FileUtils.mkdir(info_dir_path) unless File.exist?(info_dir_path)
+ File.write(File.join(info_dir_path, 'attributes'), "*.md -diff\n")
end
- FileUtils.mkdir(info_dir_path) unless File.exist?(info_dir_path)
- File.write(File.join(info_dir_path, 'attributes'), "*.md -diff\n")
+ it "returns true for files that do not have attributes" do
+ diff_file = diffs.diff_file_with_new_path('LICENSE')
+ expect(diff_file.diffable?).to be_truthy
+ end
+
+ it "returns false for files that have been marked as not being diffable in attributes" do
+ diff_file = diffs.diff_file_with_new_path('README.md')
+ expect(diff_file.diffable?).to be_falsey
+ end
end
- it "returns true for files that do not have attributes" do
- diff_file = diffs.diff_file_with_new_path('LICENSE')
- expect(diff_file.diffable?).to be_truthy
+ context 'when the text has binary notice' do
+ let(:commit) { project.commit('f05a98786e4274708e1fa118c7ad3a29d1d1b9a3') }
+ let(:diff_file) { commit.diffs.diff_file_with_new_path('VERSION') }
+
+ it "returns false" do
+ expect(diff_file.diffable?).to be_falsey
+ end
end
- it "returns false for files that have been marked as not being diffable in attributes" do
- diff_file = diffs.diff_file_with_new_path('README.md')
- expect(diff_file.diffable?).to be_falsey
+ context 'when the content is binary' do
+ let(:commit) { project.commit('2f63565e7aac07bcdadb654e253078b727143ec4') }
+ let(:diff_file) { commit.diffs.diff_file_with_new_path('files/images/6049019_460s.jpg') }
+
+ it "returns true" do
+ expect(diff_file.diffable?).to be_truthy
+ end
end
end
@@ -729,6 +749,18 @@ RSpec.describe Gitlab::Diff::File do
end
end
+ context 'when the the encoding of the file is unsupported' do
+ let(:commit) { project.commit('f05a98786e4274708e1fa118c7ad3a29d1d1b9a3') }
+ let(:diff_file) { commit.diffs.diff_file_with_new_path('VERSION') }
+
+ it 'returns a Not Diffable viewer' do
+ expect(diff_file.simple_viewer).to be_a(DiffViewer::NotDiffable)
+ end
+
+ it { expect(diff_file.highlighted_diff_lines).to eq([]) }
+ it { expect(diff_file.parallel_diff_lines).to eq([]) }
+ end
+
describe '#diff_hunk' do
context 'when first line is a match' do
let(:raw_diff) do
diff --git a/spec/lib/gitlab/diff/parser_spec.rb b/spec/lib/gitlab/diff/parser_spec.rb
index 7448ae0b2ea..c8069f82f04 100644
--- a/spec/lib/gitlab/diff/parser_spec.rb
+++ b/spec/lib/gitlab/diff/parser_spec.rb
@@ -146,6 +146,16 @@ eos
it { expect(parser.parse(nil)).to eq([]) }
end
+ context 'when it is a binary notice' do
+ let(:diff) do
+ <<~END
+ Binary files a/test and b/test differ
+ END
+ end
+
+ it { expect(parser.parse(diff.each_line)).to eq([]) }
+ end
+
describe 'tolerates special diff markers in a content' do
it "counts lines correctly" do
diff = <<~END
diff --git a/spec/lib/gitlab/http_spec.rb b/spec/lib/gitlab/http_spec.rb
index 308f7f46251..71e80de9f89 100644
--- a/spec/lib/gitlab/http_spec.rb
+++ b/spec/lib/gitlab/http_spec.rb
@@ -27,6 +27,47 @@ RSpec.describe Gitlab::HTTP do
end
end
+ context 'when reading the response is too slow' do
+ before do
+ stub_const("#{described_class}::DEFAULT_READ_TOTAL_TIMEOUT", 0.001.seconds)
+
+ WebMock.stub_request(:post, /.*/).to_return do |request|
+ sleep 0.002.seconds
+ { body: 'I\m slow', status: 200 }
+ end
+ end
+
+ let(:options) { {} }
+
+ subject(:request_slow_responder) { described_class.post('http://example.org', **options) }
+
+ specify do
+ expect { request_slow_responder }.not_to raise_error
+ end
+
+ context 'with use_read_total_timeout option' do
+ let(:options) { { use_read_total_timeout: true } }
+
+ it 'raises a timeout error' do
+ expect { request_slow_responder }.to raise_error(Gitlab::HTTP::ReadTotalTimeout, /Request timed out after ?([0-9]*[.])?[0-9]+ seconds/)
+ end
+
+ context 'and timeout option' do
+ let(:options) { { use_read_total_timeout: true, timeout: 10.seconds } }
+
+ it 'overrides the default timeout when timeout option is present' do
+ expect { request_slow_responder }.not_to raise_error
+ end
+ end
+ end
+ end
+
+ it 'calls a block' do
+ WebMock.stub_request(:post, /.*/)
+
+ expect { |b| described_class.post('http://example.org', &b) }.to yield_with_args
+ end
+
describe 'allow_local_requests_from_web_hooks_and_services is' do
before do
WebMock.stub_request(:get, /.*/).to_return(status: 200, body: 'Success')
diff --git a/spec/models/audit_event_spec.rb b/spec/models/audit_event_spec.rb
index 5c87c2e68db..bc603bc5ab6 100644
--- a/spec/models/audit_event_spec.rb
+++ b/spec/models/audit_event_spec.rb
@@ -3,9 +3,6 @@
require 'spec_helper'
RSpec.describe AuditEvent do
- let_it_be(:audit_event) { create(:project_audit_event) }
- subject { audit_event }
-
describe 'validations' do
include_examples 'validates IP address' do
let(:attribute) { :ip_address }
@@ -13,6 +10,15 @@ RSpec.describe AuditEvent do
end
end
+ it 'sanitizes custom_message in the details hash' do
+ audit_event = create(:project_audit_event, details: { target_id: 678, custom_message: '<strong>Arnold</strong>' })
+
+ expect(audit_event.details).to include(
+ target_id: 678,
+ custom_message: 'Arnold'
+ )
+ end
+
describe '#as_json' do
context 'ip_address' do
subject { build(:group_audit_event, ip_address: '192.168.1.1').as_json }
diff --git a/spec/models/protected_branch/push_access_level_spec.rb b/spec/models/protected_branch/push_access_level_spec.rb
index 17a589f0485..fa84cd660cb 100644
--- a/spec/models/protected_branch/push_access_level_spec.rb
+++ b/spec/models/protected_branch/push_access_level_spec.rb
@@ -44,7 +44,7 @@ RSpec.describe ProtectedBranch::PushAccessLevel do
let(:can_push) { true }
before_all do
- project.add_guest(user)
+ project.add_maintainer(user)
end
context 'when this push_access_level is tied to a deploy key' do
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index e5c86e69ffc..2185df0609e 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -387,6 +387,19 @@ RSpec.describe User do
expect(user.errors.full_messages).to eq(['Username has already been taken'])
end
end
+
+ it 'validates format' do
+ Mime::EXTENSION_LOOKUP.keys.each do |type|
+ user = build(:user, username: "test.#{type}")
+
+ expect(user).not_to be_valid
+ expect(user.errors.full_messages).to include('Username ending with MIME type format is not allowed.')
+ end
+ end
+
+ it 'validates format on updated record' do
+ expect(create(:user).update(username: 'profile.html')).to be_falsey
+ end
end
it 'has a DB-level NOT NULL constraint on projects_limit' do
@@ -2882,7 +2895,7 @@ RSpec.describe User do
end
describe '#sanitize_attrs' do
- let(:user) { build(:user, name: 'test & user', skype: 'test&user') }
+ let(:user) { build(:user, name: 'test <& user', skype: 'test&user') }
it 'encodes HTML entities in the Skype attribute' do
expect { user.sanitize_attrs }.to change { user.skype }.to('test&amp;user')
@@ -2891,6 +2904,25 @@ RSpec.describe User do
it 'does not encode HTML entities in the name attribute' do
expect { user.sanitize_attrs }.not_to change { user.name }
end
+
+ it 'sanitizes attr from html tags' do
+ user = create(:user, name: '<a href="//example.com">Test<a>', twitter: '<a href="//evil.com">https://twitter.com<a>')
+
+ expect(user.name).to eq('Test')
+ expect(user.twitter).to eq('https://twitter.com')
+ end
+
+ it 'sanitizes attr from js scripts' do
+ user = create(:user, name: '<script>alert("Test")</script>')
+
+ expect(user.name).to eq("alert(\"Test\")")
+ end
+
+ it 'sanitizes attr from iframe scripts' do
+ user = create(:user, name: 'User"><iframe src=javascript:alert()><iframe>')
+
+ expect(user.name).to eq('User">')
+ end
end
describe '#starred?' do
diff --git a/spec/policies/global_policy_spec.rb b/spec/policies/global_policy_spec.rb
index e88619b9527..85026ced466 100644
--- a/spec/policies/global_policy_spec.rb
+++ b/spec/policies/global_policy_spec.rb
@@ -239,6 +239,14 @@ RSpec.describe GlobalPolicy do
it { is_expected.not_to be_allowed(:access_api) }
end
+ context 'with a deactivated user' do
+ before do
+ current_user.deactivate!
+ end
+
+ it { is_expected.not_to be_allowed(:access_api) }
+ end
+
context 'user with expired password' do
before do
current_user.update!(password_expires_at: 2.minutes.ago)
diff --git a/spec/requests/api/graphql_spec.rb b/spec/requests/api/graphql_spec.rb
index a336d74b135..463fca43cb5 100644
--- a/spec/requests/api/graphql_spec.rb
+++ b/spec/requests/api/graphql_spec.rb
@@ -6,6 +6,9 @@ RSpec.describe 'GraphQL' do
include AfterNextHelpers
let(:query) { graphql_query_for('echo', text: 'Hello world') }
+ let(:mutation) { 'mutation { echoCreate(input: { messages: ["hello", "world"] }) { echoes } }' }
+
+ let_it_be(:user) { create(:user) }
describe 'logging' do
shared_examples 'logging a graphql query' do
@@ -70,6 +73,139 @@ RSpec.describe 'GraphQL' do
end
end
+ context 'when executing mutations' do
+ let(:mutation_with_variables) do
+ <<~GQL
+ mutation($a: String!, $b: String!) {
+ echoCreate(input: { messages: [$a, $b] }) { echoes }
+ }
+ GQL
+ end
+
+ context 'with POST' do
+ it 'succeeds' do
+ post_graphql(mutation, current_user: user)
+
+ expect(graphql_data_at(:echo_create, :echoes)).to eq %w[hello world]
+ end
+
+ context 'with variables' do
+ it 'succeeds' do
+ post_graphql(mutation_with_variables, current_user: user, variables: { a: 'Yo', b: 'there' })
+
+ expect(graphql_data_at(:echo_create, :echoes)).to eq %w[Yo there]
+ end
+ end
+ end
+
+ context 'with GET' do
+ it 'fails' do
+ get_graphql(mutation, current_user: user)
+
+ expect(graphql_errors).to include(a_hash_including('message' => /Mutations are forbidden/))
+ end
+
+ context 'with variables' do
+ it 'fails' do
+ get_graphql(mutation_with_variables, current_user: user, variables: { a: 'Yo', b: 'there' })
+
+ expect(graphql_errors).to include(a_hash_including('message' => /Mutations are forbidden/))
+ end
+ end
+ end
+ end
+
+ context 'when executing queries' do
+ context 'with POST' do
+ it 'succeeds' do
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data_at(:echo)).to include 'Hello world'
+ end
+ end
+
+ context 'with GET' do
+ it 'succeeds' do
+ get_graphql(query, current_user: user)
+
+ expect(graphql_data_at(:echo)).to include 'Hello world'
+ end
+ end
+ end
+
+ context 'when selecting a query by operation name' do
+ let(:query) { "query A #{graphql_query_for('echo', text: 'Hello world')}" }
+ let(:mutation) { 'mutation B { echoCreate(input: { messages: ["hello", "world"] }) { echoes } }' }
+
+ let(:combined) { [query, mutation].join("\n\n") }
+
+ context 'with POST' do
+ it 'succeeds when selecting the query' do
+ post_graphql(combined, current_user: user, params: { operationName: 'A' })
+
+ resp = json_response
+
+ expect(resp.dig('data', 'echo')).to include 'Hello world'
+ end
+
+ it 'succeeds when selecting the mutation' do
+ post_graphql(combined, current_user: user, params: { operationName: 'B' })
+
+ resp = json_response
+
+ expect(resp.dig('data', 'echoCreate', 'echoes')).to eq %w[hello world]
+ end
+ end
+
+ context 'with GET' do
+ it 'succeeds when selecting the query' do
+ get_graphql(combined, current_user: user, params: { operationName: 'A' })
+
+ resp = json_response
+
+ expect(resp.dig('data', 'echo')).to include 'Hello world'
+ end
+
+ it 'fails when selecting the mutation' do
+ get_graphql(combined, current_user: user, params: { operationName: 'B' })
+
+ resp = json_response
+
+ expect(resp.dig('errors', 0, 'message')).to include "Mutations are forbidden"
+ end
+ end
+ end
+
+ context 'when batching mutations and queries' do
+ let(:batched) do
+ [
+ { query: "query A #{graphql_query_for('echo', text: 'Hello world')}" },
+ { query: 'mutation B { echoCreate(input: { messages: ["hello", "world"] }) { echoes } }' }
+ ]
+ end
+
+ context 'with POST' do
+ it 'succeeds' do
+ post_multiplex(batched, current_user: user)
+
+ resp = json_response
+
+ expect(resp.dig(0, 'data', 'echo')).to include 'Hello world'
+ expect(resp.dig(1, 'data', 'echoCreate', 'echoes')).to eq %w[hello world]
+ end
+ end
+
+ context 'with GET' do
+ it 'fails with a helpful error message' do
+ get_multiplex(batched, current_user: user)
+
+ resp = json_response
+
+ expect(resp.dig('errors', 0, 'message')).to include "Mutations are forbidden"
+ end
+ end
+ end
+
context 'with invalid variables' do
it 'returns an error' do
post_graphql(query, variables: "This is not JSON")
@@ -80,8 +216,6 @@ RSpec.describe 'GraphQL' do
end
describe 'authentication', :allow_forgery_protection do
- let(:user) { create(:user) }
-
it 'allows access to public data without authentication' do
post_graphql(query)
@@ -109,11 +243,9 @@ RSpec.describe 'GraphQL' do
context 'with token authentication' do
let(:token) { create(:personal_access_token) }
- before do
+ it 'authenticates users with a PAT' do
stub_authentication_activity_metrics(debug: false)
- end
- it 'authenticates users with a PAT' do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
.and increment(:user_session_override_counter)
@@ -124,6 +256,14 @@ RSpec.describe 'GraphQL' do
expect(graphql_data['echo']).to eq("\"#{token.user.username}\" says: Hello world")
end
+ it 'prevents access by deactived users' do
+ token.user.deactivate!
+
+ post_graphql(query, headers: { 'PRIVATE-TOKEN' => token.token })
+
+ expect(graphql_errors).to include({ 'message' => /API not accessible/ })
+ end
+
context 'when the personal access token has no api scope' do
it 'does not log the user in' do
token.update!(scopes: [:read_user])
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index e7e26c34a83..529a75af122 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -56,7 +56,7 @@ RSpec.describe API::Projects do
let_it_be(:project, reload: true) { create(:project, :repository, create_branch: 'something_else', namespace: user.namespace) }
let_it_be(:project2, reload: true) { create(:project, namespace: user.namespace) }
let_it_be(:project_member) { create(:project_member, :developer, user: user3, project: project) }
- let_it_be(:user4) { create(:user, username: 'user.with.dot') }
+ let_it_be(:user4) { create(:user, username: 'user.withdot') }
let_it_be(:project3, reload: true) do
create(:project,
:private,
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index a9231b65c8f..d724cb9612c 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe API::Users do
let_it_be(:admin) { create(:admin) }
- let_it_be(:user, reload: true) { create(:user, username: 'user.with.dot') }
+ let_it_be(:user, reload: true) { create(:user, username: 'user.withdot') }
let_it_be(:key) { create(:key, user: user) }
let_it_be(:gpg_key) { create(:gpg_key, user: user) }
let_it_be(:email) { create(:email, user: user) }
diff --git a/spec/requests/ide_controller_spec.rb b/spec/requests/ide_controller_spec.rb
index 9b0d8dcd828..4bf1e43ba40 100644
--- a/spec/requests/ide_controller_spec.rb
+++ b/spec/requests/ide_controller_spec.rb
@@ -3,7 +3,14 @@
require 'spec_helper'
RSpec.describe IdeController do
- let_it_be(:project) { create(:project, :public) }
+ let_it_be(:reporter) { create(:user) }
+
+ let_it_be(:project) do
+ create(:project, :private).tap do |p|
+ p.add_reporter(reporter)
+ end
+ end
+
let_it_be(:creator) { project.creator }
let_it_be(:other_user) { create(:user) }
@@ -14,48 +21,62 @@ RSpec.describe IdeController do
sign_in(user)
end
- it 'increases the views counter' do
- expect(Gitlab::UsageDataCounters::WebIdeCounter).to receive(:increment_views_count)
-
- get ide_url
- end
-
describe '#index', :aggregate_failures do
subject { get route }
- shared_examples 'user cannot push code' do
- include ProjectForksHelper
-
- let(:user) { other_user }
+ shared_examples 'user access rights check' do
+ context 'user can read project' do
+ it 'increases the views counter' do
+ expect(Gitlab::UsageDataCounters::WebIdeCounter).to receive(:increment_views_count)
- context 'when user does not have fork' do
- it 'instantiates fork_info instance var with fork_path and return 200' do
subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(assigns(:project)).to eq project
- expect(assigns(:fork_info)).to eq({ fork_path: controller.helpers.ide_fork_and_edit_path(project, branch, '', with_notice: false) })
end
- it 'has nil fork_info if user cannot fork' do
- project.project_feature.update!(forking_access_level: ProjectFeature::DISABLED)
+ context 'user can read project but cannot push code' do
+ include ProjectForksHelper
- subject
+ let(:user) { reporter }
- expect(response).to have_gitlab_http_status(:ok)
- expect(assigns(:fork_info)).to be_nil
+ context 'when user does not have fork' do
+ it 'instantiates fork_info instance var with fork_path and returns 200' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(assigns(:project)).to eq project
+ expect(assigns(:fork_info)).to eq({ fork_path: controller.helpers.ide_fork_and_edit_path(project, branch, '', with_notice: false) })
+ end
+
+ it 'has nil fork_info if user cannot fork' do
+ project.project_feature.update!(forking_access_level: ProjectFeature::DISABLED)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(assigns(:fork_info)).to be_nil
+ end
+ end
+
+ context 'when user has fork' do
+ let!(:fork) { fork_project(project, user, repository: true, namespace: user.namespace) }
+
+ it 'instantiates fork_info instance var with ide_path and returns 200' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(assigns(:project)).to eq project
+ expect(assigns(:fork_info)).to eq({ ide_path: controller.helpers.ide_edit_path(fork, branch, '') })
+ end
+ end
end
end
- context 'when user has fork' do
- let!(:fork) { fork_project(project, user, repository: true, namespace: user.namespace) }
+ context 'user cannot read project' do
+ let(:user) { other_user }
- it 'instantiates fork_info instance var with ide_path and return 200' do
+ it 'returns 404' do
subject
- expect(response).to have_gitlab_http_status(:ok)
- expect(assigns(:project)).to eq project
- expect(assigns(:fork_info)).to eq({ ide_path: controller.helpers.ide_edit_path(fork, branch, '') })
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
end
@@ -63,37 +84,27 @@ RSpec.describe IdeController do
context '/-/ide' do
let(:route) { '/-/ide' }
- it 'does not instantiate any instance var and return 200' do
+ it 'returns 404' do
subject
- expect(response).to have_gitlab_http_status(:ok)
- expect(assigns(:project)).to be_nil
- expect(assigns(:branch)).to be_nil
- expect(assigns(:path)).to be_nil
- expect(assigns(:merge_request)).to be_nil
- expect(assigns(:fork_info)).to be_nil
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
context '/-/ide/project' do
let(:route) { '/-/ide/project' }
- it 'does not instantiate any instance var and return 200' do
+ it 'returns 404' do
subject
- expect(response).to have_gitlab_http_status(:ok)
- expect(assigns(:project)).to be_nil
- expect(assigns(:branch)).to be_nil
- expect(assigns(:path)).to be_nil
- expect(assigns(:merge_request)).to be_nil
- expect(assigns(:fork_info)).to be_nil
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
context '/-/ide/project/:project' do
let(:route) { "/-/ide/project/#{project.full_path}" }
- it 'instantiates project instance var and return 200' do
+ it 'instantiates project instance var and returns 200' do
subject
expect(response).to have_gitlab_http_status(:ok)
@@ -104,13 +115,13 @@ RSpec.describe IdeController do
expect(assigns(:fork_info)).to be_nil
end
- it_behaves_like 'user cannot push code'
+ it_behaves_like 'user access rights check'
%w(edit blob tree).each do |action|
context "/-/ide/project/:project/#{action}" do
let(:route) { "/-/ide/project/#{project.full_path}/#{action}" }
- it 'instantiates project instance var and return 200' do
+ it 'instantiates project instance var and returns 200' do
subject
expect(response).to have_gitlab_http_status(:ok)
@@ -121,13 +132,13 @@ RSpec.describe IdeController do
expect(assigns(:fork_info)).to be_nil
end
- it_behaves_like 'user cannot push code'
+ it_behaves_like 'user access rights check'
context "/-/ide/project/:project/#{action}/:branch" do
let(:branch) { 'master' }
let(:route) { "/-/ide/project/#{project.full_path}/#{action}/#{branch}" }
- it 'instantiates project and branch instance vars and return 200' do
+ it 'instantiates project and branch instance vars and returns 200' do
subject
expect(response).to have_gitlab_http_status(:ok)
@@ -138,13 +149,13 @@ RSpec.describe IdeController do
expect(assigns(:fork_info)).to be_nil
end
- it_behaves_like 'user cannot push code'
+ it_behaves_like 'user access rights check'
context "/-/ide/project/:project/#{action}/:branch/-" do
let(:branch) { 'branch/slash' }
let(:route) { "/-/ide/project/#{project.full_path}/#{action}/#{branch}/-" }
- it 'instantiates project and branch instance vars and return 200' do
+ it 'instantiates project and branch instance vars and returns 200' do
subject
expect(response).to have_gitlab_http_status(:ok)
@@ -155,13 +166,13 @@ RSpec.describe IdeController do
expect(assigns(:fork_info)).to be_nil
end
- it_behaves_like 'user cannot push code'
+ it_behaves_like 'user access rights check'
context "/-/ide/project/:project/#{action}/:branch/-/:path" do
let(:branch) { 'master' }
let(:route) { "/-/ide/project/#{project.full_path}/#{action}/#{branch}/-/foo/.bar" }
- it 'instantiates project, branch, and path instance vars and return 200' do
+ it 'instantiates project, branch, and path instance vars and returns 200' do
subject
expect(response).to have_gitlab_http_status(:ok)
@@ -172,7 +183,7 @@ RSpec.describe IdeController do
expect(assigns(:fork_info)).to be_nil
end
- it_behaves_like 'user cannot push code'
+ it_behaves_like 'user access rights check'
end
end
end
@@ -184,7 +195,7 @@ RSpec.describe IdeController do
let(:route) { "/-/ide/project/#{project.full_path}/merge_requests/#{merge_request.id}" }
- it 'instantiates project and merge_request instance vars and return 200' do
+ it 'instantiates project and merge_request instance vars and returns 200' do
subject
expect(response).to have_gitlab_http_status(:ok)
@@ -195,7 +206,7 @@ RSpec.describe IdeController do
expect(assigns(:fork_info)).to be_nil
end
- it_behaves_like 'user cannot push code'
+ it_behaves_like 'user access rights check'
end
end
end
diff --git a/spec/services/feature_flags/create_service_spec.rb b/spec/services/feature_flags/create_service_spec.rb
index 2e0c162ebc1..4eb2b25fb64 100644
--- a/spec/services/feature_flags/create_service_spec.rb
+++ b/spec/services/feature_flags/create_service_spec.rb
@@ -68,12 +68,12 @@ RSpec.describe FeatureFlags::CreateService do
end
it 'creates audit event' do
- expected_message = 'Created feature flag <strong>feature_flag</strong> '\
- 'with description <strong>"description"</strong>. '\
- 'Created rule <strong>*</strong> and set it as <strong>active</strong> '\
- 'with strategies <strong>[{"name"=>"default", "parameters"=>{}}]</strong>. '\
- 'Created rule <strong>production</strong> and set it as <strong>inactive</strong> '\
- 'with strategies <strong>[{"name"=>"default", "parameters"=>{}}]</strong>.'
+ expected_message = 'Created feature flag feature_flag '\
+ 'with description "description". '\
+ 'Created rule * and set it as active '\
+ 'with strategies [{"name"=&gt;"default", "parameters"=&gt;{}}]. '\
+ 'Created rule production and set it as inactive '\
+ 'with strategies [{"name"=&gt;"default", "parameters"=&gt;{}}].'
expect { subject }.to change { AuditEvent.count }.by(1)
expect(AuditEvent.last.details[:custom_message]).to eq(expected_message)
diff --git a/spec/services/feature_flags/destroy_service_spec.rb b/spec/services/feature_flags/destroy_service_spec.rb
index ee30474873c..d3796ef6b4d 100644
--- a/spec/services/feature_flags/destroy_service_spec.rb
+++ b/spec/services/feature_flags/destroy_service_spec.rb
@@ -33,7 +33,7 @@ RSpec.describe FeatureFlags::DestroyService do
it 'creates audit log' do
expect { subject }.to change { AuditEvent.count }.by(1)
- expect(audit_event_message).to eq("Deleted feature flag <strong>#{feature_flag.name}</strong>.")
+ expect(audit_event_message).to eq("Deleted feature flag #{feature_flag.name}.")
end
context 'when user is reporter' do
diff --git a/spec/services/feature_flags/update_service_spec.rb b/spec/services/feature_flags/update_service_spec.rb
index d838549891a..4858139d60a 100644
--- a/spec/services/feature_flags/update_service_spec.rb
+++ b/spec/services/feature_flags/update_service_spec.rb
@@ -38,9 +38,9 @@ RSpec.describe FeatureFlags::UpdateService do
expect { subject }.to change { AuditEvent.count }.by(1)
expect(audit_event_message).to(
- eq("Updated feature flag <strong>new_name</strong>. "\
- "Updated name from <strong>\"#{name_was}\"</strong> "\
- "to <strong>\"new_name\"</strong>.")
+ eq("Updated feature flag new_name. "\
+ "Updated name from \"#{name_was}\" "\
+ "to \"new_name\".")
)
end
@@ -94,8 +94,8 @@ RSpec.describe FeatureFlags::UpdateService do
it 'creates audit event with changed description' do
expect { subject }.to change { AuditEvent.count }.by(1)
expect(audit_event_message).to(
- include("Updated description from <strong>\"\"</strong>"\
- " to <strong>\"new description\"</strong>.")
+ include("Updated description from \"\""\
+ " to \"new description\".")
)
end
end
@@ -110,7 +110,7 @@ RSpec.describe FeatureFlags::UpdateService do
it 'creates audit event about changing active state' do
expect { subject }.to change { AuditEvent.count }.by(1)
expect(audit_event_message).to(
- include('Updated active from <strong>"true"</strong> to <strong>"false"</strong>.')
+ include('Updated active from "true" to "false".')
)
end
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index 276656656ec..d710e4a777f 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -184,14 +184,6 @@ RSpec.describe Projects::ForkService do
end
end
- context 'GitLab CI is enabled' do
- it "forks and enables CI for fork" do
- @from_project.enable_ci
- @to_project = fork_project(@from_project, @to_user, using_service: true)
- expect(@to_project.builds_enabled?).to be_truthy
- end
- end
-
context "CI/CD settings" do
let(:to_project) { fork_project(@from_project, @to_user, using_service: true) }
@@ -366,6 +358,19 @@ RSpec.describe Projects::ForkService do
expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
end
+
+ it 'copies project features visibility settings to the fork', :aggregate_failures do
+ attrs = ProjectFeature::FEATURES.to_h do |f|
+ ["#{f}_access_level", ProjectFeature::PRIVATE]
+ end
+
+ public_project.project_feature.update!(attrs)
+
+ user = create(:user, developer_projects: [public_project])
+ forked_project = described_class.new(public_project, user).execute
+
+ expect(forked_project.project_feature.slice(attrs.keys)).to eq(attrs)
+ end
end
end
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index 4857fa63114..38cf828ca5e 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -400,8 +400,13 @@ module GraphqlHelpers
post api('/', current_user, version: 'graphql'), params: { _json: queries }, headers: headers
end
- def post_graphql(query, current_user: nil, variables: nil, headers: {}, token: {})
- params = { query: query, variables: serialize_variables(variables) }
+ def get_multiplex(queries, current_user: nil, headers: {})
+ path = "/?#{queries.to_query('_json')}"
+ get api(path, current_user, version: 'graphql'), headers: headers
+ end
+
+ def post_graphql(query, current_user: nil, variables: nil, headers: {}, token: {}, params: {})
+ params = params.merge(query: query, variables: serialize_variables(variables))
post api('/', current_user, version: 'graphql', **token), params: params, headers: headers
return unless graphql_errors
@@ -410,6 +415,18 @@ module GraphqlHelpers
expect(graphql_errors).not_to include(a_hash_including('message' => 'Internal server error'))
end
+ def get_graphql(query, current_user: nil, variables: nil, headers: {}, token: {}, params: {})
+ vars = "variables=#{CGI.escape(serialize_variables(variables))}" if variables
+ params = params.to_a.map { |k, v| v.to_query(k) }
+ path = ["/?query=#{CGI.escape(query)}", vars, *params].join('&')
+ get api(path, current_user, version: 'graphql', **token), headers: headers
+
+ return unless graphql_errors
+
+ # Errors are acceptable, but not this one:
+ expect(graphql_errors).not_to include(a_hash_including('message' => 'Internal server error'))
+ end
+
def post_graphql_mutation(mutation, current_user: nil, token: {})
post_graphql(mutation.query,
current_user: current_user,