Commit 5f77f0df authored by Robert Speicher's avatar Robert Speicher

Merge branch 'master' of dev.gitlab.org:gitlab/gitlab-ee

parents 98b2b872 3e8fc446
...@@ -2,6 +2,10 @@ ...@@ -2,6 +2,10 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 14.6.2 (2022-01-10)
No changes.
## 14.6.1 (2022-01-04) ## 14.6.1 (2022-01-04)
### Fixed (2 changes) ### Fixed (2 changes)
...@@ -387,6 +391,10 @@ entry. ...@@ -387,6 +391,10 @@ entry.
- [Fix OpenStruct use](gitlab-org/gitlab@f8466f5943a1afeabaf9cf781f7804a8df515a0e) by @mehulsharma ([merge request](gitlab-org/gitlab!74702)) - [Fix OpenStruct use](gitlab-org/gitlab@f8466f5943a1afeabaf9cf781f7804a8df515a0e) by @mehulsharma ([merge request](gitlab-org/gitlab!74702))
- [Update Sidekiq to 6.3.1](gitlab-org/gitlab@22e8bc0af656717e56428a7227c467fe08021c66) ([merge request](gitlab-org/gitlab!73973)) - [Update Sidekiq to 6.3.1](gitlab-org/gitlab@22e8bc0af656717e56428a7227c467fe08021c66) ([merge request](gitlab-org/gitlab!73973))
## 14.5.3 (2022-01-11)
No changes.
## 14.5.2 (2021-12-03) ## 14.5.2 (2021-12-03)
No changes. No changes.
...@@ -929,6 +937,10 @@ No changes. ...@@ -929,6 +937,10 @@ No changes.
- [Add pipeline artifacts and uploads sizes to project REST API](gitlab-org/gitlab@58d66f28faf42ae98ca11ff1ba0bdd9180e988ad) by @guillaume.chauvel ([merge request](gitlab-org/gitlab!72075)) - [Add pipeline artifacts and uploads sizes to project REST API](gitlab-org/gitlab@58d66f28faf42ae98ca11ff1ba0bdd9180e988ad) by @guillaume.chauvel ([merge request](gitlab-org/gitlab!72075))
- [Remove not used parameter from epics finder](gitlab-org/gitlab@49fce172b57b2f376a114726b1dd1900fe36a238) ([merge request](gitlab-org/gitlab!72285)) **GitLab Enterprise Edition** - [Remove not used parameter from epics finder](gitlab-org/gitlab@49fce172b57b2f376a114726b1dd1900fe36a238) ([merge request](gitlab-org/gitlab!72285)) **GitLab Enterprise Edition**
## 14.4.5 (2022-01-11)
No changes.
## 14.4.4 (2021-12-03) ## 14.4.4 (2021-12-03)
No changes. No changes.
3627c14a64ce48446e8a67299c3161ff7290d1ad e02b0d67e48ed5a4493b073c9836d376a780f34d
...@@ -64,10 +64,12 @@ class GlEmoji extends HTMLElement { ...@@ -64,10 +64,12 @@ class GlEmoji extends HTMLElement {
this.classList.add('emoji-icon'); this.classList.add('emoji-icon');
this.classList.add(fallbackSpriteClass); this.classList.add(fallbackSpriteClass);
} else if (hasImageFallback) { } else if (hasImageFallback) {
this.innerHTML = emojiImageTag(name, fallbackSrc); this.innerHTML = '';
this.appendChild(emojiImageTag(name, fallbackSrc));
} else { } else {
const src = emojiFallbackImageSrc(name); const src = emojiFallbackImageSrc(name);
this.innerHTML = emojiImageTag(name, src); this.innerHTML = '';
this.appendChild(emojiImageTag(name, src));
} }
} }
}); });
......
import { escape, minBy } from 'lodash'; import { escape, minBy } from 'lodash';
import emojiRegexFactory from 'emoji-regex'; import emojiRegexFactory from 'emoji-regex';
import emojiAliases from 'emojis/aliases.json'; import emojiAliases from 'emojis/aliases.json';
import { setAttributes } from '~/lib/utils/dom_utils';
import AccessorUtilities from '../lib/utils/accessor'; import AccessorUtilities from '../lib/utils/accessor';
import axios from '../lib/utils/axios_utils'; import axios from '../lib/utils/axios_utils';
import { CACHE_KEY, CACHE_VERSION_KEY, CATEGORY_ICON_MAP, FREQUENTLY_USED_KEY } from './constants'; import { CACHE_KEY, CACHE_VERSION_KEY, CATEGORY_ICON_MAP, FREQUENTLY_USED_KEY } from './constants';
...@@ -220,7 +221,19 @@ export function emojiFallbackImageSrc(inputName) { ...@@ -220,7 +221,19 @@ export function emojiFallbackImageSrc(inputName) {
} }
export function emojiImageTag(name, src) { export function emojiImageTag(name, src) {
return `<img class="emoji" title=":${name}:" alt=":${name}:" src="${src}" width="20" height="20" align="absmiddle" />`; const img = document.createElement('img');
img.className = 'emoji';
setAttributes(img, {
title: `:${name}:`,
alt: `:${name}:`,
src,
width: '20',
height: '20',
align: 'absmiddle',
});
return img;
} }
export function glEmojiTag(inputName, options) { export function glEmojiTag(inputName, options) {
......
...@@ -20,7 +20,7 @@ module SessionlessAuthentication ...@@ -20,7 +20,7 @@ module SessionlessAuthentication
end end
def sessionless_sign_in(user) def sessionless_sign_in(user)
if user && can?(user, :log_in) if can?(user, :log_in) && !user.password_expired_if_applicable?
# Notice we are passing store false, so the user is not # Notice we are passing store false, so the user is not
# actually stored in the session and a token is needed # actually stored in the session and a token is needed
# for every request. If you want the token to work as a # for every request. If you want the token to work as a
......
...@@ -28,8 +28,14 @@ class Import::GithubController < Import::BaseController ...@@ -28,8 +28,14 @@ class Import::GithubController < Import::BaseController
end end
def callback def callback
session[access_token_key] = get_token(params[:code]) auth_state = session[auth_state_key]
redirect_to status_import_url session[auth_state_key] = nil
if auth_state.blank? || !ActiveSupport::SecurityUtils.secure_compare(auth_state, params[:state])
provider_unauthorized
else
session[access_token_key] = get_token(params[:code])
redirect_to status_import_url
end
end end
def personal_access_token def personal_access_token
...@@ -154,13 +160,16 @@ class Import::GithubController < Import::BaseController ...@@ -154,13 +160,16 @@ class Import::GithubController < Import::BaseController
end end
def authorize_url def authorize_url
state = SecureRandom.base64(64)
session[auth_state_key] = state
if Feature.enabled?(:remove_legacy_github_client) if Feature.enabled?(:remove_legacy_github_client)
oauth_client.auth_code.authorize_url( oauth_client.auth_code.authorize_url(
redirect_uri: callback_import_url, redirect_uri: callback_import_url,
scope: 'repo, user, user:email' scope: 'repo, user, user:email',
state: state
) )
else else
client.authorize_url(callback_import_url) client.authorize_url(callback_import_url, state)
end end
end end
...@@ -219,6 +228,10 @@ class Import::GithubController < Import::BaseController ...@@ -219,6 +228,10 @@ class Import::GithubController < Import::BaseController
alert: _('Missing OAuth configuration for GitHub.') alert: _('Missing OAuth configuration for GitHub.')
end end
def auth_state_key
:"#{provider_name}_auth_state_key"
end
def access_token_key def access_token_key
:"#{provider_name}_access_token" :"#{provider_name}_access_token"
end end
......
...@@ -36,7 +36,7 @@ module Resolvers ...@@ -36,7 +36,7 @@ module Resolvers
def unconditional_includes def unconditional_includes
[ [
{ {
project: [:project_feature] project: [:project_feature, :group]
}, },
:author :author
] ]
......
...@@ -6,6 +6,13 @@ module Integrations ...@@ -6,6 +6,13 @@ module Integrations
def notify(message, opts) def notify(message, opts)
# See https://gitlab.com/gitlab-org/slack-notifier/#custom-http-client # See https://gitlab.com/gitlab-org/slack-notifier/#custom-http-client
#
# TODO: By default both Markdown and HTML links are converted into Slack "mrkdwn" syntax,
# but it seems we only need to support Markdown and could disable HTML.
#
# See:
# - https://gitlab.com/gitlab-org/slack-notifier#middleware
# - https://gitlab.com/gitlab-org/gitlab/-/issues/347048
notifier = ::Slack::Messenger.new(webhook, opts.merge(http_client: HTTPClient)) notifier = ::Slack::Messenger.new(webhook, opts.merge(http_client: HTTPClient))
notifier.ping( notifier.ping(
message.pretext, message.pretext,
......
...@@ -23,7 +23,7 @@ module Integrations ...@@ -23,7 +23,7 @@ module Integrations
def attachments def attachments
[{ [{
title: title, title: strip_markup(title),
title_link: alert_url, title_link: alert_url,
color: attachment_color, color: attachment_color,
fields: attachment_fields fields: attachment_fields
...@@ -31,7 +31,7 @@ module Integrations ...@@ -31,7 +31,7 @@ module Integrations
end end
def message def message
"Alert firing in #{project_name}" "Alert firing in #{strip_markup(project_name)}"
end end
private private
......
...@@ -5,6 +5,10 @@ module Integrations ...@@ -5,6 +5,10 @@ module Integrations
class BaseMessage class BaseMessage
RELATIVE_LINK_REGEX = %r{!\[[^\]]*\]\((/uploads/[^\)]*)\)}.freeze RELATIVE_LINK_REGEX = %r{!\[[^\]]*\]\((/uploads/[^\)]*)\)}.freeze
# Markup characters which are used for links in HTML, Markdown,
# and Slack "mrkdwn" syntax (`<http://example.com|Label>`).
UNSAFE_MARKUP_CHARACTERS = '<>[]|'
attr_reader :markdown attr_reader :markdown
attr_reader :user_full_name attr_reader :user_full_name
attr_reader :user_name attr_reader :user_name
...@@ -65,12 +69,26 @@ module Integrations ...@@ -65,12 +69,26 @@ module Integrations
string.gsub(RELATIVE_LINK_REGEX, "#{project_url}\\1") string.gsub(RELATIVE_LINK_REGEX, "#{project_url}\\1")
end end
# Remove unsafe markup from user input, which can be used to hijack links in our own markup,
# or insert new ones.
#
# This currently removes Markdown and Slack "mrkdwn" links (keeping the link label),
# and all HTML markup (keeping the text nodes).
# We can't just escape the markup characters, because each chat app handles this differently.
#
# See:
# - https://api.slack.com/reference/surfaces/formatting#escaping
# - https://gitlab.com/gitlab-org/slack-notifier#escaping
def strip_markup(string)
string&.delete(UNSAFE_MARKUP_CHARACTERS)
end
def attachment_color def attachment_color
'#345' '#345'
end end
def link(text, url) def link(text, url)
"[#{text}](#{url})" "[#{strip_markup(text)}](#{url})"
end end
def pretty_duration(seconds) def pretty_duration(seconds)
......
...@@ -27,7 +27,7 @@ module Integrations ...@@ -27,7 +27,7 @@ module Integrations
def attachments def attachments
[{ [{
text: "#{project_link} with job #{deployment_link} by #{user_link}\n#{commit_link}: #{commit_title}", text: "#{project_link} with job #{deployment_link} by #{user_link}\n#{commit_link}: #{strip_markup(commit_title)}",
color: color color: color
}] }]
end end
...@@ -40,9 +40,9 @@ module Integrations ...@@ -40,9 +40,9 @@ module Integrations
def message def message
if running? if running?
"Starting deploy to #{environment}" "Starting deploy to #{strip_markup(environment)}"
else else
"Deploy to #{environment} #{humanized_status}" "Deploy to #{strip_markup(environment)} #{humanized_status}"
end end
end end
......
...@@ -32,7 +32,7 @@ module Integrations ...@@ -32,7 +32,7 @@ module Integrations
def activity def activity
{ {
title: "Issue #{state} by #{user_combined_name}", title: "Issue #{state} by #{strip_markup(user_combined_name)}",
subtitle: "in #{project_link}", subtitle: "in #{project_link}",
text: issue_link, text: issue_link,
image: user_avatar image: user_avatar
...@@ -42,7 +42,7 @@ module Integrations ...@@ -42,7 +42,7 @@ module Integrations
private private
def message def message
"[#{project_link}] Issue #{issue_link} #{state} by #{user_combined_name}" "[#{project_link}] Issue #{issue_link} #{state} by #{strip_markup(user_combined_name)}"
end end
def opened_issue? def opened_issue?
...@@ -67,7 +67,7 @@ module Integrations ...@@ -67,7 +67,7 @@ module Integrations
end end
def issue_title def issue_title
"#{Issue.reference_prefix}#{issue_iid} #{title}" "#{Issue.reference_prefix}#{issue_iid} #{strip_markup(title)}"
end end
end end
end end
......
...@@ -29,7 +29,7 @@ module Integrations ...@@ -29,7 +29,7 @@ module Integrations
def activity def activity
{ {
title: "Merge request #{state_or_action_text} by #{user_combined_name}", title: "Merge request #{state_or_action_text} by #{strip_markup(user_combined_name)}",
subtitle: "in #{project_link}", subtitle: "in #{project_link}",
text: merge_request_link, text: merge_request_link,
image: user_avatar image: user_avatar
...@@ -39,7 +39,7 @@ module Integrations ...@@ -39,7 +39,7 @@ module Integrations
private private
def format_title(title) def format_title(title)
'*' + title.lines.first.chomp + '*' '*' + strip_markup(title.lines.first.chomp) + '*'
end end
def message def message
...@@ -51,7 +51,7 @@ module Integrations ...@@ -51,7 +51,7 @@ module Integrations
end end
def merge_request_message def merge_request_message
"#{user_combined_name} #{state_or_action_text} merge request #{merge_request_link} in #{project_link}" "#{strip_markup(user_combined_name)} #{state_or_action_text} merge request #{merge_request_link} in #{project_link}"
end end
def merge_request_link def merge_request_link
...@@ -59,7 +59,7 @@ module Integrations ...@@ -59,7 +59,7 @@ module Integrations
end end
def merge_request_title def merge_request_title
"#{MergeRequest.reference_prefix}#{merge_request_iid} #{title}" "#{MergeRequest.reference_prefix}#{merge_request_iid} #{strip_markup(title)}"
end end
def merge_request_url def merge_request_url
......
...@@ -35,9 +35,9 @@ module Integrations ...@@ -35,9 +35,9 @@ module Integrations
def activity def activity
{ {
title: "#{user_combined_name} #{link('commented on ' + target, note_url)}", title: "#{strip_markup(user_combined_name)} #{link('commented on ' + target, note_url)}",
subtitle: "in #{project_link}", subtitle: "in #{project_link}",
text: formatted_title, text: strip_markup(formatted_title),
image: user_avatar image: user_avatar
} }
end end
...@@ -45,7 +45,7 @@ module Integrations ...@@ -45,7 +45,7 @@ module Integrations
private private
def message def message
"#{user_combined_name} #{link('commented on ' + target, note_url)} in #{project_link}: *#{formatted_title}*" "#{strip_markup(user_combined_name)} #{link('commented on ' + target, note_url)} in #{project_link}: *#{strip_markup(formatted_title)}*"
end end
def format_title(title) def format_title(title)
......
...@@ -56,7 +56,7 @@ module Integrations ...@@ -56,7 +56,7 @@ module Integrations
[{ [{
fallback: format(message), fallback: format(message),
color: attachment_color, color: attachment_color,
author_name: user_combined_name, author_name: strip_markup(user_combined_name),
author_icon: user_avatar, author_icon: user_avatar,
author_link: author_url, author_link: author_url,
title: s_("ChatMessage|Pipeline #%{pipeline_id} %{humanized_status} in %{duration}") % title: s_("ChatMessage|Pipeline #%{pipeline_id} %{humanized_status} in %{duration}") %
...@@ -80,7 +80,7 @@ module Integrations ...@@ -80,7 +80,7 @@ module Integrations
pipeline_link: pipeline_link, pipeline_link: pipeline_link,
ref_type: ref_type, ref_type: ref_type,
ref_link: ref_link, ref_link: ref_link,
user_combined_name: user_combined_name, user_combined_name: strip_markup(user_combined_name),
humanized_status: humanized_status humanized_status: humanized_status
}, },
subtitle: s_("ChatMessage|in %{project_link}") % { project_link: project_link }, subtitle: s_("ChatMessage|in %{project_link}") % { project_link: project_link },
...@@ -154,7 +154,7 @@ module Integrations ...@@ -154,7 +154,7 @@ module Integrations
pipeline_link: pipeline_link, pipeline_link: pipeline_link,
ref_type: ref_type, ref_type: ref_type,
ref_link: ref_link, ref_link: ref_link,
user_combined_name: user_combined_name, user_combined_name: strip_markup(user_combined_name),
humanized_status: humanized_status, humanized_status: humanized_status,
duration: pretty_duration(duration) duration: pretty_duration(duration)
} }
...@@ -189,7 +189,7 @@ module Integrations ...@@ -189,7 +189,7 @@ module Integrations
end end
def ref_link def ref_link
"[#{ref}](#{ref_url})" link(ref, ref_url)
end end
def project_url def project_url
...@@ -197,7 +197,7 @@ module Integrations ...@@ -197,7 +197,7 @@ module Integrations
end end
def project_link def project_link
"[#{project.name}](#{project_url})" link(project.name, project_url)
end end
def pipeline_failed_jobs_url def pipeline_failed_jobs_url
...@@ -213,7 +213,7 @@ module Integrations ...@@ -213,7 +213,7 @@ module Integrations
end end
def pipeline_link def pipeline_link
"[##{pipeline_id}](#{pipeline_url})" link("##{pipeline_id}", pipeline_url)
end end
def job_url(job) def job_url(job)
...@@ -221,7 +221,7 @@ module Integrations ...@@ -221,7 +221,7 @@ module Integrations
end end
def job_link(job) def job_link(job)
"[#{job[:name]}](#{job_url(job)})" link(job[:name], job_url(job))
end end
def failed_jobs_links def failed_jobs_links
...@@ -242,7 +242,7 @@ module Integrations ...@@ -242,7 +242,7 @@ module Integrations
def stage_link(stage) def stage_link(stage)
# All stages link to the pipeline page # All stages link to the pipeline page
"[#{stage}](#{pipeline_url})" link(stage, pipeline_url)
end end
def failed_stages_links def failed_stages_links
...@@ -254,7 +254,7 @@ module Integrations ...@@ -254,7 +254,7 @@ module Integrations
end end
def commit_link def commit_link
"[#{commit.title}](#{commit_url})" link(commit.title, commit_url)
end end
def author_url def author_url
......
...@@ -39,7 +39,7 @@ module Integrations ...@@ -39,7 +39,7 @@ module Integrations
def humanized_action(short: false) def humanized_action(short: false)
action, ref_link, target_link = compose_action_details action, ref_link, target_link = compose_action_details
text = [user_combined_name, action, ref_type, ref_link] text = [strip_markup(user_combined_name), action, ref_type, ref_link]
text << target_link unless short text << target_link unless short
text.join(' ') text.join(' ')
end end
...@@ -67,7 +67,7 @@ module Integrations ...@@ -67,7 +67,7 @@ module Integrations
url = commit[:url] url = commit[:url]
"[#{id}](#{url}): #{title} - #{author}" "#{link(id, url)}: #{strip_markup(title)} - #{strip_markup(author)}"
end end
def new_branch? def new_branch?
...@@ -91,15 +91,15 @@ module Integrations ...@@ -91,15 +91,15 @@ module Integrations
end end
def ref_link def ref_link
"[#{ref}](#{ref_url})" link(ref, ref_url)
end end
def project_link def project_link
"[#{project_name}](#{project_url})" link(project_name, project_url)
end end
def compare_link def compare_link
"[Compare changes](#{compare_url})" link('Compare changes', compare_url)
end end
def compose_action_details def compose_action_details
......
...@@ -36,9 +36,9 @@ module Integrations ...@@ -36,9 +36,9 @@ module Integrations
def activity def activity
{ {
title: "#{user_combined_name} #{action} #{wiki_page_link}", title: "#{strip_markup(user_combined_name)} #{action} #{wiki_page_link}",
subtitle: "in #{project_link}", subtitle: "in #{project_link}",
text: title, text: strip_markup(title),
image: user_avatar image: user_avatar
} }
end end
...@@ -46,7 +46,7 @@ module Integrations ...@@ -46,7 +46,7 @@ module Integrations
private private
def message def message
"#{user_combined_name} #{action} #{wiki_page_link} (#{diff_link}) in #{project_link}: *#{title}*" "#{strip_markup(user_combined_name)} #{action} #{wiki_page_link} (#{diff_link}) in #{project_link}: *#{strip_markup(title)}*"
end end
def description_message def description_message
......
# frozen_string_literal: true
# Archive Extraction Service allows extraction of contents
# from `tar` archives with an additional handling (removal)
# of file symlinks.
#
# @param tmpdir [String] A path where archive is located
# and where its contents are extracted.
# Tmpdir directory must be located under `Dir.tmpdir`.
# `BulkImports::Error` is raised if any other directory path is used.
#
# @param filename [String] Name of the file to extract contents from.
#
# @example
# dir = Dir.mktmpdir
# filename = 'things.tar'
# BulkImports::ArchiveExtractionService.new(tmpdir: dir, filename: filename).execute
# Dir.glob(File.join(dir, '**', '*'))
# => ['/path/to/tmp/dir/extracted_file_1', '/path/to/tmp/dir/extracted_file_2', '/path/to/tmp/dir/extracted_file_3']
module BulkImports
class ArchiveExtractionService
include Gitlab::ImportExport::CommandLineUtil
def initialize(tmpdir:, filename:)
@tmpdir = tmpdir
@filename = filename
@filepath = File.join(@tmpdir, @filename)
end
def execute
validate_filepath
validate_tmpdir
validate_symlink
extract_archive
remove_symlinks
tmpdir
end
private
attr_reader :tmpdir, :filename, :filepath
def validate_filepath
Gitlab::Utils.check_path_traversal!(filepath)
end
def validate_tmpdir
raise(BulkImports::Error, 'Invalid target directory') unless File.expand_path(tmpdir).start_with?(Dir.tmpdir)
end
def validate_symlink
raise(BulkImports::Error, 'Invalid file') if symlink?(filepath)
end
def symlink?(filepath)
File.lstat(filepath).symlink?
end
def extract_archive
untar_xf(archive: filepath, dir: tmpdir)
end
def extracted_files
Dir.glob(File.join(tmpdir, '**', '*'))
end
def remove_symlinks
extracted_files.each do |path|
FileUtils.rm(path) if symlink?(path)
end
end
end
end
...@@ -70,10 +70,24 @@ module Packages ...@@ -70,10 +70,24 @@ module Packages
end end
end end
# TODO (technical debt): Extract the package size calculation to its own component and unit test it separately.
def calculated_package_file_size
strong_memoize(:calculated_package_file_size) do
# This calculation is based on:
# 1. 4 chars in a Base64 encoded string are 3 bytes in the original string. Meaning 1 char is 0.75 bytes.
# 2. The encoded string may have 1 or 2 extra '=' chars used for padding. Each padding char means 1 byte less in the original string.
# Reference:
# - https://blog.aaronlenoir.com/2017/11/10/get-original-length-from-base-64-string/
# - https://en.wikipedia.org/wiki/Base64#Decoding_Base64_with_padding
encoded_data = attachment['data']
((encoded_data.length * 0.75 ) - encoded_data[-2..].count('=')).to_i
end
end
def file_params def file_params
{ {
file: CarrierWaveStringFile.new(Base64.decode64(attachment['data'])), file: CarrierWaveStringFile.new(Base64.decode64(attachment['data'])),
size: attachment['length'], size: calculated_package_file_size,
file_sha1: version_data[:dist][:shasum], file_sha1: version_data[:dist][:shasum],
file_name: package_file_name, file_name: package_file_name,
build: params[:build] build: params[:build]
...@@ -86,7 +100,7 @@ module Packages ...@@ -86,7 +100,7 @@ module Packages
end end
def file_size_exceeded? def file_size_exceeded?
project.actual_limits.exceeded?(:npm_max_file_size, attachment['length'].to_i) project.actual_limits.exceeded?(:npm_max_file_size, calculated_package_file_size)
end end
end end
end end
......
...@@ -2,13 +2,17 @@ ...@@ -2,13 +2,17 @@
module Resolvers module Resolvers
class SecurityReportSummaryResolver < BaseResolver class SecurityReportSummaryResolver < BaseResolver
type Types::SecurityReportSummaryType, null: true include Gitlab::Graphql::Authorize::AuthorizeResource
type Types::SecurityReportSummaryType, null: true
authorize :read_security_resource
extras [:lookahead] extras [:lookahead]
alias_method :pipeline, :object alias_method :pipeline, :object
def resolve(lookahead:) def resolve(lookahead:)
return unless authorized_resource?(pipeline.project)
Security::ReportSummaryService.new( Security::ReportSummaryService.new(
pipeline, pipeline,
selection_information(lookahead) selection_information(lookahead)
......
# frozen_string_literal: true # frozen_string_literal: true
module Types module Types
# rubocop: disable Graphql/AuthorizeTypes
class PipelineSecurityReportFindingType < BaseObject class PipelineSecurityReportFindingType < BaseObject
graphql_name 'PipelineSecurityReportFinding' graphql_name 'PipelineSecurityReportFinding'
description 'Represents vulnerability finding of a security report on the pipeline.' description 'Represents vulnerability finding of a security report on the pipeline.'
authorize :read_security_resource
field :report_type, field :report_type,
type: VulnerabilityReportTypeEnum, type: VulnerabilityReportTypeEnum,
null: true, null: true,
...@@ -97,5 +98,4 @@ module Types ...@@ -97,5 +98,4 @@ module Types
object.project.licensed_feature_available?(:sast_fp_reduction) object.project.licensed_feature_available?(:sast_fp_reduction)
end end
end end
# rubocop: enable Graphql/AuthorizeTypes
end end
...@@ -110,6 +110,8 @@ module Vulnerabilities ...@@ -110,6 +110,8 @@ module Vulnerabilities
.where("vulnerability_occurrences.location -> 'kubernetes_resource' -> 'agent_id' ?| array[:agent_ids]", agent_ids: agent_ids) .where("vulnerability_occurrences.location -> 'kubernetes_resource' -> 'agent_id' ?| array[:agent_ids]", agent_ids: agent_ids)
end end
alias_method :declarative_policy_subject, :project
def self.counted_by_severity def self.counted_by_severity
group(:severity).count.transform_keys do |severity| group(:severity).count.transform_keys do |severity|
severities[severity] severities[severity]
......
...@@ -328,6 +328,7 @@ module EE ...@@ -328,6 +328,7 @@ module EE
rule { ip_enforcement_prevents_access & ~owner & ~auditor }.policy do rule { ip_enforcement_prevents_access & ~owner & ~auditor }.policy do
prevent :read_group prevent :read_group
prevent :read_milestone
end end
rule { owner & group_saml_enabled }.policy do rule { owner & group_saml_enabled }.policy do
......
...@@ -342,6 +342,10 @@ module EE ...@@ -342,6 +342,10 @@ module EE
rule { ip_enforcement_prevents_access & ~admin & ~auditor }.policy do rule { ip_enforcement_prevents_access & ~admin & ~auditor }.policy do
prevent :read_project prevent :read_project
prevent :read_issue
prevent :read_merge_request
prevent :read_milestone
prevent :read_container_image
end end
rule { locked_approvers_rules }.policy do rule { locked_approvers_rules }.policy do
......
...@@ -5,9 +5,14 @@ require 'spec_helper' ...@@ -5,9 +5,14 @@ require 'spec_helper'
RSpec.describe Resolvers::SecurityReportSummaryResolver do RSpec.describe Resolvers::SecurityReportSummaryResolver do
include GraphqlHelpers include GraphqlHelpers
let_it_be(:pipeline) { 'pipeline' } let_it_be(:pipeline) { create(:ci_pipeline) }
let_it_be(:user) { pipeline.project.owner }
describe '#resolve' do describe '#resolve' do
before do
stub_licensed_features(sast: true, dependency_scanning: true, container_scanning: true, dast: true, security_dashboard: true)
end
context 'All fields are requested' do context 'All fields are requested' do
let(:lookahead) do let(:lookahead) do
build_mock_lookahead(expected_selection_info) build_mock_lookahead(expected_selection_info)
...@@ -32,7 +37,18 @@ RSpec.describe Resolvers::SecurityReportSummaryResolver do ...@@ -32,7 +37,18 @@ RSpec.describe Resolvers::SecurityReportSummaryResolver do
) do |summary_service| ) do |summary_service|
expect(summary_service).to receive(:execute).and_return({}) expect(summary_service).to receive(:execute).and_return({})
end end
resolve(described_class, obj: pipeline, lookahead: lookahead)
resolve_security_report_summary
end
context 'when the user is not authorized' do
let_it_be(:user) { create(:user) }
it 'does not call Security::ReportSummaryService and returns nothing' do
stub_const('Security::ReportSummaryService', double)
expect(resolve_security_report_summary).to be_nil
end
end end
end end
...@@ -61,10 +77,15 @@ RSpec.describe Resolvers::SecurityReportSummaryResolver do ...@@ -61,10 +77,15 @@ RSpec.describe Resolvers::SecurityReportSummaryResolver do
) do |summary_service| ) do |summary_service|
expect(summary_service).to receive(:execute).and_return({}) expect(summary_service).to receive(:execute).and_return({})
end end
resolve(described_class, obj: pipeline, lookahead: lookahead)
resolve_security_report_summary
end end
end end
end end
def resolve_security_report_summary
resolve(described_class, obj: pipeline, lookahead: lookahead, ctx: { current_user: user })
end
end end
def build_mock_lookahead(structure) def build_mock_lookahead(structure)
......
...@@ -33,6 +33,7 @@ RSpec.describe GitlabSchema.types['PipelineSecurityReportFinding'] do ...@@ -33,6 +33,7 @@ RSpec.describe GitlabSchema.types['PipelineSecurityReportFinding'] do
subject { GitlabSchema.execute(query, context: { current_user: user }).as_json } subject { GitlabSchema.execute(query, context: { current_user: user }).as_json }
specify { expect(described_class.graphql_name).to eq('PipelineSecurityReportFinding') } specify { expect(described_class.graphql_name).to eq('PipelineSecurityReportFinding') }
specify { expect(described_class).to require_graphql_authorizations(:read_security_resource) }
it { expect(described_class).to have_graphql_fields(fields) } it { expect(described_class).to have_graphql_fields(fields) }
......
...@@ -575,6 +575,7 @@ RSpec.describe GroupPolicy do ...@@ -575,6 +575,7 @@ RSpec.describe GroupPolicy do
context 'without restriction' do context 'without restriction' do
it { is_expected.to be_allowed(:read_group) } it { is_expected.to be_allowed(:read_group) }
it { is_expected.to be_allowed(:read_milestone) }
end end
context 'with restriction' do context 'with restriction' do
...@@ -586,6 +587,7 @@ RSpec.describe GroupPolicy do ...@@ -586,6 +587,7 @@ RSpec.describe GroupPolicy do
let(:range) { '192.168.0.0/24' } let(:range) { '192.168.0.0/24' }
it { is_expected.to be_allowed(:read_group) } it { is_expected.to be_allowed(:read_group) }
it { is_expected.to be_allowed(:read_milestone) }
end end
context 'address is outside the range' do context 'address is outside the range' do
...@@ -593,18 +595,21 @@ RSpec.describe GroupPolicy do ...@@ -593,18 +595,21 @@ RSpec.describe GroupPolicy do
context 'as developer' do context 'as developer' do
it { is_expected.to be_disallowed(:read_group) } it { is_expected.to be_disallowed(:read_group) }
it { is_expected.to be_disallowed(:read_milestone) }
end end
context 'as owner' do context 'as owner' do
let(:current_user) { owner } let(:current_user) { owner }
it { is_expected.to be_allowed(:read_group) } it { is_expected.to be_allowed(:read_group) }
it { is_expected.to be_allowed(:read_milestone) }
end end
context 'as auditor' do context 'as auditor' do
let(:current_user) { create(:user, :auditor) } let(:current_user) { create(:user, :auditor) }
it { is_expected.to be_allowed(:read_group) } it { is_expected.to be_allowed(:read_group) }
it { is_expected.to be_allowed(:read_milestone) }
end end
end end
end end
......
...@@ -455,6 +455,10 @@ RSpec.describe ProjectPolicy do ...@@ -455,6 +455,10 @@ RSpec.describe ProjectPolicy do
context 'group without restriction' do context 'group without restriction' do
it { is_expected.to be_allowed(:read_project) } it { is_expected.to be_allowed(:read_project) }
it { is_expected.to be_allowed(:read_issue) }
it { is_expected.to be_allowed(:read_merge_request) }
it { is_expected.to be_allowed(:read_milestone) }
it { is_expected.to be_allowed(:read_container_image) }
end end
context 'group with restriction' do context 'group with restriction' do
...@@ -466,25 +470,45 @@ RSpec.describe ProjectPolicy do ...@@ -466,25 +470,45 @@ RSpec.describe ProjectPolicy do
let(:range) { '192.168.0.0/24' } let(:range) { '192.168.0.0/24' }
it { is_expected.to be_allowed(:read_project) } it { is_expected.to be_allowed(:read_project) }
it { is_expected.to be_allowed(:read_issue) }
it { is_expected.to be_allowed(:read_merge_request) }
it { is_expected.to be_allowed(:read_milestone) }
it { is_expected.to be_allowed(:read_container_image) }
end end
context 'address is outside the range' do context 'address is outside the range' do
let(:range) { '10.0.0.0/8' } let(:range) { '10.0.0.0/8' }
it { is_expected.to be_disallowed(:read_project) } it { is_expected.to be_disallowed(:read_project) }
it { is_expected.to be_disallowed(:read_issue) }
it { is_expected.to be_disallowed(:read_merge_request) }
it { is_expected.to be_disallowed(:read_milestone) }
it { is_expected.to be_disallowed(:read_container_image) }
context 'with admin enabled', :enable_admin_mode do context 'with admin enabled', :enable_admin_mode do
it { is_expected.to be_allowed(:read_project) } it { is_expected.to be_allowed(:read_project) }
it { is_expected.to be_allowed(:read_issue) }
it { is_expected.to be_allowed(:read_merge_request) }
it { is_expected.to be_allowed(:read_milestone) }
it { is_expected.to be_allowed(:read_container_image) }
end end
context 'with admin disabled' do context 'with admin disabled' do
it { is_expected.to be_disallowed(:read_project) } it { is_expected.to be_disallowed(:read_project) }
it { is_expected.to be_disallowed(:read_issue) }
it { is_expected.to be_disallowed(:read_merge_request) }
it { is_expected.to be_disallowed(:read_milestone) }
it { is_expected.to be_disallowed(:read_container_image) }
end end
context 'with auditor' do context 'with auditor' do
let(:current_user) { create(:user, :auditor) } let(:current_user) { create(:user, :auditor) }
it { is_expected.to be_allowed(:read_project) } it { is_expected.to be_allowed(:read_project) }
it { is_expected.to be_allowed(:read_issue) }
it { is_expected.to be_allowed(:read_merge_request) }
it { is_expected.to be_allowed(:read_milestone) }
it { is_expected.to be_allowed(:read_container_image) }
end end
end end
end end
......
...@@ -3,7 +3,9 @@ ...@@ -3,7 +3,9 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe 'Query.project(fullPath).pipeline(iid).securityReportFindings' do RSpec.describe 'Query.project(fullPath).pipeline(iid).securityReportFindings' do
let_it_be(:project) { create(:project, :repository) } include GraphqlHelpers
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:pipeline) { create(:ci_pipeline, :success, project: project) } let_it_be(:pipeline) { create(:ci_pipeline, :success, project: project) }
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
...@@ -49,13 +51,16 @@ RSpec.describe 'Query.project(fullPath).pipeline(iid).securityReportFindings' do ...@@ -49,13 +51,16 @@ RSpec.describe 'Query.project(fullPath).pipeline(iid).securityReportFindings' do
) )
end end
subject { GitlabSchema.execute(query, context: { current_user: user }).as_json } let(:security_report_findings) { subject.dig('project', 'pipeline', 'securityReportFindings', 'nodes') }
let(:security_report_findings) { subject.dig('data', 'project', 'pipeline', 'securityReportFindings', 'nodes') } subject do
post_graphql(query, current_user: user)
graphql_data
end
context 'when `sast` and `dast` features are enabled' do context 'when the required features are enabled' do
before do before do
stub_licensed_features(sast: true, dast: true) stub_licensed_features(sast: true, dast: true, security_dashboard: true)
end end
context 'when user is member of the project' do context 'when user is member of the project' do
...@@ -86,18 +91,18 @@ RSpec.describe 'Query.project(fullPath).pipeline(iid).securityReportFindings' do ...@@ -86,18 +91,18 @@ RSpec.describe 'Query.project(fullPath).pipeline(iid).securityReportFindings' do
context 'when user is not a member of the project' do context 'when user is not a member of the project' do
it 'returns no vulnerability findings' do it 'returns no vulnerability findings' do
expect(security_report_findings).to be_nil expect(security_report_findings).to be_blank
end end
end end
end end
context 'when `sast` and `dast` both features are disabled' do context 'when the required features are disabled' do
before do before do
stub_licensed_features(sast: false, dast: false) stub_licensed_features(sast: false, dast: false, security_dashboard: false)
end end
it 'returns no vulnerability findings' do it 'returns no vulnerability findings' do
expect(security_report_findings).to be_nil expect(security_report_findings).to be_blank
end end
end end
end end
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe 'Query.project(fullPath).pipeline(iid).securityReportSummary' do RSpec.describe 'Query.project(fullPath).pipeline(iid).securityReportSummary' do
include GraphqlHelpers
let_it_be(:project) { create(:project, :repository) } let_it_be(:project) { create(:project, :repository) }
let_it_be(:pipeline) { create(:ci_pipeline, :success, project: project) } let_it_be(:pipeline) { create(:ci_pipeline, :success, project: project) }
...@@ -65,28 +67,54 @@ RSpec.describe 'Query.project(fullPath).pipeline(iid).securityReportSummary' do ...@@ -65,28 +67,54 @@ RSpec.describe 'Query.project(fullPath).pipeline(iid).securityReportSummary' do
) )
end end
before do let(:security_report_summary) { subject.dig('project', 'pipeline', 'securityReportSummary') }
stub_licensed_features(sast: true, dependency_scanning: true, container_scanning: true, dast: true)
project.add_developer(user) subject do
post_graphql(query, current_user: user)
graphql_data
end end
subject { GitlabSchema.execute(query, context: { current_user: user }).as_json } context 'when the required features are enabled' do
before do
stub_licensed_features(sast: true, dependency_scanning: true, container_scanning: true, dast: true, security_dashboard: true)
end
context 'when user is member of the project' do
before do
project.add_developer(user)
end
it 'shows the vulnerabilitiesCount and scannedResourcesCount' do
expect(security_report_summary.dig('dast', 'vulnerabilitiesCount')).to eq(20)
expect(security_report_summary.dig('dast', 'scannedResourcesCount')).to eq(26)
expect(security_report_summary.dig('sast', 'vulnerabilitiesCount')).to eq(5)
end
let(:security_report_summary) { subject.dig('data', 'project', 'pipeline', 'securityReportSummary') } it 'shows the first 20 scanned resources' do
dast_scanned_resources = security_report_summary.dig('dast', 'scannedResources', 'nodes')
it 'shows the vulnerabilitiesCount and scannedResourcesCount' do expect(dast_scanned_resources.length).to eq(20)
expect(security_report_summary.dig('dast', 'vulnerabilitiesCount')).to eq(20) end
expect(security_report_summary.dig('dast', 'scannedResourcesCount')).to eq(26)
expect(security_report_summary.dig('sast', 'vulnerabilitiesCount')).to eq(5)
end
it 'shows the first 20 scanned resources' do it 'returns nil for the scannedResourcesCsvPath' do
dast_scanned_resources = security_report_summary.dig('dast', 'scannedResources', 'nodes') expect(security_report_summary.dig('dast', 'scannedResourcesCsvPath')).to be_nil
end
end
expect(dast_scanned_resources.length).to eq(20) context 'when user is not a member of the project' do
it 'returns no scanned resources' do
expect(security_report_summary).to be_nil
end
end
end end
it 'returns nil for the scannedResourcesCsvPath' do context 'when the required features are disabled' do
expect(security_report_summary.dig('dast', 'scannedResourcesCsvPath')).to be_nil before do
stub_licensed_features(sast: false, dependency_scanning: false, container_scanning: false, dast: false, security_dashboard: false)
end
it 'returns no scanned resources' do
expect(security_report_summary).to be_nil
end
end end
end end
...@@ -614,6 +614,7 @@ module API ...@@ -614,6 +614,7 @@ module API
source_project = Project.find_by_id(params[:project_id]) source_project = Project.find_by_id(params[:project_id])
not_found!('Project') unless source_project && can?(current_user, :read_project, source_project) not_found!('Project') unless source_project && can?(current_user, :read_project, source_project)
forbidden!('Project') unless source_project && can?(current_user, :admin_project_member, source_project)
result = ::Members::ImportProjectTeamService.new(current_user, params).execute result = ::Members::ImportProjectTeamService.new(current_user, params).execute
......
...@@ -5,16 +5,16 @@ module BulkImports ...@@ -5,16 +5,16 @@ module BulkImports
module Pipelines module Pipelines
class UploadsPipeline class UploadsPipeline
include Pipeline include Pipeline
include Gitlab::ImportExport::CommandLineUtil
FILENAME = 'uploads.tar.gz'
AVATAR_PATTERN = %r{.*\/#{BulkImports::UploadsExportService::AVATAR_PATH}\/(?<identifier>.*)}.freeze AVATAR_PATTERN = %r{.*\/#{BulkImports::UploadsExportService::AVATAR_PATH}\/(?<identifier>.*)}.freeze
AvatarLoadingError = Class.new(StandardError) AvatarLoadingError = Class.new(StandardError)
def extract(context) def extract(_context)
download_service(tmp_dir, context).execute download_service.execute
untar_zxf(archive: File.join(tmp_dir, FILENAME), dir: tmp_dir) decompression_service.execute
extraction_service.execute
upload_file_paths = Dir.glob(File.join(tmp_dir, '**', '*')) upload_file_paths = Dir.glob(File.join(tmp_dir, '**', '*'))
BulkImports::Pipeline::ExtractedData.new(data: upload_file_paths) BulkImports::Pipeline::ExtractedData.new(data: upload_file_paths)
...@@ -29,6 +29,7 @@ module BulkImports ...@@ -29,6 +29,7 @@ module BulkImports
return unless dynamic_path return unless dynamic_path
return if File.directory?(file_path) return if File.directory?(file_path)
return if File.lstat(file_path).symlink?
named_captures = dynamic_path.named_captures.symbolize_keys named_captures = dynamic_path.named_captures.symbolize_keys
...@@ -36,20 +37,40 @@ module BulkImports ...@@ -36,20 +37,40 @@ module BulkImports
end end
def after_run(_) def after_run(_)
FileUtils.remove_entry(tmp_dir) FileUtils.remove_entry(tmp_dir) if Dir.exist?(tmp_dir)
end end
private private
def download_service(tmp_dir, context) def download_service
BulkImports::FileDownloadService.new( BulkImports::FileDownloadService.new(
configuration: context.configuration, configuration: context.configuration,
relative_url: context.entity.relation_download_url_path('uploads'), relative_url: context.entity.relation_download_url_path(relation),
dir: tmp_dir, dir: tmp_dir,
filename: FILENAME filename: targz_filename
) )
end end
def decompression_service
BulkImports::FileDecompressionService.new(dir: tmp_dir, filename: targz_filename)
end
def extraction_service
BulkImports::ArchiveExtractionService.new(tmpdir: tmp_dir, filename: tar_filename)
end
def relation
BulkImports::FileTransfer::BaseConfig::UPLOADS_RELATION
end
def tar_filename
"#{relation}.tar"
end
def targz_filename
"#{tar_filename}.gz"
end
def tmp_dir def tmp_dir
@tmp_dir ||= Dir.mktmpdir('bulk_imports') @tmp_dir ||= Dir.mktmpdir('bulk_imports')
end end
......
...@@ -18,6 +18,10 @@ module Gitlab ...@@ -18,6 +18,10 @@ module Gitlab
tar_with_options(archive: archive, dir: dir, options: 'cf') tar_with_options(archive: archive, dir: dir, options: 'cf')
end end
def untar_xf(archive:, dir:)
untar_with_options(archive: archive, dir: dir, options: 'xf')
end
def gzip(dir:, filename:) def gzip(dir:, filename:)
gzip_with_options(dir: dir, filename: filename) gzip_with_options(dir: dir, filename: filename)
end end
......
...@@ -48,10 +48,11 @@ module Gitlab ...@@ -48,10 +48,11 @@ module Gitlab
) )
end end
def authorize_url(redirect_uri) def authorize_url(redirect_uri, state = nil)
client.auth_code.authorize_url({ client.auth_code.authorize_url({
redirect_uri: redirect_uri, redirect_uri: redirect_uri,
scope: "repo, user, user:email" scope: "repo, user, user:email",
state: state
}) })
end end
......
...@@ -252,13 +252,13 @@ module Gitlab ...@@ -252,13 +252,13 @@ module Gitlab
def internal_web?(uri) def internal_web?(uri)
uri.scheme == config.gitlab.protocol && uri.scheme == config.gitlab.protocol &&
uri.hostname == config.gitlab.host && uri.hostname == config.gitlab.host &&
(uri.port.blank? || uri.port == config.gitlab.port) get_port(uri) == config.gitlab.port
end end
def internal_shell?(uri) def internal_shell?(uri)
uri.scheme == 'ssh' && uri.scheme == 'ssh' &&
uri.hostname == config.gitlab_shell.ssh_host && uri.hostname == config.gitlab_shell.ssh_host &&
(uri.port.blank? || uri.port == config.gitlab_shell.ssh_port) get_port(uri) == config.gitlab_shell.ssh_port
end end
def domain_allowed?(uri) def domain_allowed?(uri)
......
...@@ -8,10 +8,6 @@ RSpec.describe Dashboard::ProjectsController, :aggregate_failures do ...@@ -8,10 +8,6 @@ RSpec.describe Dashboard::ProjectsController, :aggregate_failures do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
describe '#index' do describe '#index' do
context 'user not logged in' do
it_behaves_like 'authenticates sessionless user', :index, :atom
end
context 'user logged in' do context 'user logged in' do
let_it_be(:project) { create(:project, name: 'Project 1') } let_it_be(:project) { create(:project, name: 'Project 1') }
let_it_be(:project2) { create(:project, name: 'Project Two') } let_it_be(:project2) { create(:project, name: 'Project Two') }
......
...@@ -72,9 +72,6 @@ RSpec.describe DashboardController do ...@@ -72,9 +72,6 @@ RSpec.describe DashboardController do
end end
end end
it_behaves_like 'authenticates sessionless user', :issues, :atom, author_id: User.first
it_behaves_like 'authenticates sessionless user', :issues_calendar, :ics
describe "#check_filters_presence!" do describe "#check_filters_presence!" do
let(:user) { create(:user) } let(:user) { create(:user) }
......
...@@ -1209,26 +1209,6 @@ RSpec.describe GroupsController, factory_default: :keep do ...@@ -1209,26 +1209,6 @@ RSpec.describe GroupsController, factory_default: :keep do
end end
end end
context 'token authentication' do
it_behaves_like 'authenticates sessionless user', :show, :atom, public: true do
before do
default_params.merge!(id: group)
end
end
it_behaves_like 'authenticates sessionless user', :issues, :atom, public: true do
before do
default_params.merge!(id: group, author_id: user.id)
end
end
it_behaves_like 'authenticates sessionless user', :issues_calendar, :ics, public: true do
before do
default_params.merge!(id: group)
end
end
end
describe 'external authorization' do describe 'external authorization' do
before do before do
group.add_owner(user) group.add_owner(user)
......
...@@ -6,6 +6,7 @@ RSpec.describe Import::GithubController do ...@@ -6,6 +6,7 @@ RSpec.describe Import::GithubController do
include ImportSpecHelper include ImportSpecHelper
let(:provider) { :github } let(:provider) { :github }
let(:new_import_url) { public_send("new_import_#{provider}_url") }
include_context 'a GitHub-ish import controller' include_context 'a GitHub-ish import controller'
...@@ -50,13 +51,37 @@ RSpec.describe Import::GithubController do ...@@ -50,13 +51,37 @@ RSpec.describe Import::GithubController do
stub_omniauth_provider('github') stub_omniauth_provider('github')
end end
it "updates access token" do context "when auth state param is missing from session" do
token = "asdasd12345" it "reports an error" do
get :callback
get :callback expect(controller).to redirect_to(new_import_url)
expect(flash[:alert]).to eq('Access denied to your GitHub account.')
end
end
context "when auth state param is present in session" do
let(:valid_auth_state) { "secret-state" }
before do
session[:github_auth_state_key] = valid_auth_state
end
expect(session[:github_access_token]).to eq(token) it "updates access token if state param is valid" do
expect(controller).to redirect_to(status_import_github_url) token = "asdasd12345"
get :callback, params: { state: valid_auth_state }
expect(session[:github_access_token]).to eq(token)
expect(controller).to redirect_to(status_import_github_url)
end
it "reports an error if state param is invalid" do
get :callback, params: { state: "different-state" }
expect(controller).to redirect_to(new_import_url)
expect(flash[:alert]).to eq('Access denied to your GitHub account.')
end
end end
end end
...@@ -71,8 +96,6 @@ RSpec.describe Import::GithubController do ...@@ -71,8 +96,6 @@ RSpec.describe Import::GithubController do
end end
context 'when OAuth config is missing' do context 'when OAuth config is missing' do
let(:new_import_url) { public_send("new_import_#{provider}_url") }
before do before do
allow(controller).to receive(:oauth_config).and_return(nil) allow(controller).to receive(:oauth_config).and_return(nil)
end end
...@@ -108,6 +131,16 @@ RSpec.describe Import::GithubController do ...@@ -108,6 +131,16 @@ RSpec.describe Import::GithubController do
get :status get :status
end end
it 'gets authorization url using legacy client' do
allow(controller).to receive(:logged_in_with_provider?).and_return(true)
expect(controller).to receive(:go_to_provider_for_permissions).and_call_original
expect_next_instance_of(Gitlab::LegacyGithubImport::Client) do |client|
expect(client).to receive(:authorize_url).and_call_original
end
get :new
end
end end
context 'when feature remove_legacy_github_client is enabled' do context 'when feature remove_legacy_github_client is enabled' do
...@@ -130,6 +163,16 @@ RSpec.describe Import::GithubController do ...@@ -130,6 +163,16 @@ RSpec.describe Import::GithubController do
get :status get :status
end end
it 'gets authorization url using oauth client' do
allow(controller).to receive(:logged_in_with_provider?).and_return(true)
expect(controller).to receive(:go_to_provider_for_permissions).and_call_original
expect_next_instance_of(OAuth2::Client) do |client|
expect(client.auth_code).to receive(:authorize_url).and_call_original
end
get :new
end
context 'pagination' do context 'pagination' do
context 'when no page is specified' do context 'when no page is specified' do
it 'requests first page' do it 'requests first page' do
......
...@@ -162,27 +162,4 @@ RSpec.describe Projects::CommitsController do ...@@ -162,27 +162,4 @@ RSpec.describe Projects::CommitsController do
end end
end end
end end
context 'token authentication' do
context 'public project' do
it_behaves_like 'authenticates sessionless user', :show, :atom, { public: true, ignore_incrementing: true } do
before do
public_project = create(:project, :repository, :public)
default_params.merge!(namespace_id: public_project.namespace, project_id: public_project, id: "master.atom")
end
end
end
context 'private project' do
it_behaves_like 'authenticates sessionless user', :show, :atom, { public: false, ignore_incrementing: true } do
before do
private_project = create(:project, :repository, :private)
private_project.add_maintainer(user)
default_params.merge!(namespace_id: private_project.namespace, project_id: private_project, id: "master.atom")
end
end
end
end
end end
...@@ -1948,40 +1948,4 @@ RSpec.describe Projects::IssuesController do ...@@ -1948,40 +1948,4 @@ RSpec.describe Projects::IssuesController do
end end
end end
end end
context 'private project with token authentication' do
let_it_be(:private_project) { create(:project, :private) }
it_behaves_like 'authenticates sessionless user', :index, :atom, ignore_incrementing: true do
before do
default_params.merge!(project_id: private_project, namespace_id: private_project.namespace)
private_project.add_maintainer(user)
end
end
it_behaves_like 'authenticates sessionless user', :calendar, :ics, ignore_incrementing: true do
before do
default_params.merge!(project_id: private_project, namespace_id: private_project.namespace)
private_project.add_maintainer(user)
end
end
end
context 'public project with token authentication' do
let_it_be(:public_project) { create(:project, :public) }
it_behaves_like 'authenticates sessionless user', :index, :atom, public: true do
before do
default_params.merge!(project_id: public_project, namespace_id: public_project.namespace)
end
end
it_behaves_like 'authenticates sessionless user', :calendar, :ics, public: true do
before do
default_params.merge!(project_id: public_project, namespace_id: public_project.namespace)
end
end
end
end end
...@@ -128,6 +128,8 @@ RSpec.describe Projects::RawController do ...@@ -128,6 +128,8 @@ RSpec.describe Projects::RawController do
let_it_be(:user) { create(:user, static_object_token: 'very-secure-token') } let_it_be(:user) { create(:user, static_object_token: 'very-secure-token') }
let_it_be(:file_path) { 'master/README.md' } let_it_be(:file_path) { 'master/README.md' }
let(:token) { user.static_object_token }
before do before do
project.add_developer(user) project.add_developer(user)
end end
...@@ -143,13 +145,36 @@ RSpec.describe Projects::RawController do ...@@ -143,13 +145,36 @@ RSpec.describe Projects::RawController do
context 'when a token param is present' do context 'when a token param is present' do
context 'when token is correct' do context 'when token is correct' do
let(:params) { { token: user.static_object_token } } let(:params) { { token: token } }
it 'calls the action normally' do it 'calls the action normally' do
get_show get_show
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
end end
context 'when user with expired password' do
let_it_be(:user) { create(:user, password_expires_at: 2.minutes.ago) }
it 'redirects to sign in page' do
get_show
expect(response).to have_gitlab_http_status(:found)
expect(response.location).to end_with('/users/sign_in')
end
end
context 'when password expiration is not applicable' do
context 'when ldap user' do
let_it_be(:user) { create(:omniauth_user, provider: 'ldap', password_expires_at: 2.minutes.ago) }
it 'calls the action normally' do
get_show
expect(response).to have_gitlab_http_status(:ok)
end
end
end
end end
context 'when token is incorrect' do context 'when token is incorrect' do
...@@ -165,18 +190,45 @@ RSpec.describe Projects::RawController do ...@@ -165,18 +190,45 @@ RSpec.describe Projects::RawController do
end end
context 'when a token header is present' do context 'when a token header is present' do
before do
request.headers['X-Gitlab-Static-Object-Token'] = token
end
context 'when token is correct' do context 'when token is correct' do
it 'calls the action normally' do it 'calls the action normally' do
request.headers['X-Gitlab-Static-Object-Token'] = user.static_object_token
get_show get_show
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
end end
context 'when user with expired password' do
let_it_be(:user) { create(:user, password_expires_at: 2.minutes.ago) }
it 'redirects to sign in page' do
get_show
expect(response).to have_gitlab_http_status(:found)
expect(response.location).to end_with('/users/sign_in')
end
end
context 'when password expiration is not applicable' do
context 'when ldap user' do
let_it_be(:user) { create(:omniauth_user, provider: 'ldap', password_expires_at: 2.minutes.ago) }
it 'calls the action normally' do
get_show
expect(response).to have_gitlab_http_status(:ok)
end
end
end
end end
context 'when token is incorrect' do context 'when token is incorrect' do
let(:token) { 'foobar' }
it 'redirects to sign in page' do it 'redirects to sign in page' do
request.headers['X-Gitlab-Static-Object-Token'] = 'foobar'
get_show get_show
expect(response).to have_gitlab_http_status(:found) expect(response).to have_gitlab_http_status(:found)
......
...@@ -178,6 +178,29 @@ RSpec.describe Projects::RepositoriesController do ...@@ -178,6 +178,29 @@ RSpec.describe Projects::RepositoriesController do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
end end
context 'when user with expired password' do
let_it_be(:user) { create(:user, password_expires_at: 2.minutes.ago) }
it 'redirects to sign in page' do
get :archive, params: { namespace_id: project.namespace, project_id: project, id: 'master', token: user.static_object_token }, format: 'zip'
expect(response).to have_gitlab_http_status(:found)
expect(response.location).to end_with('/users/sign_in')
end
end
context 'when password expiration is not applicable' do
context 'when ldap user' do
let_it_be(:user) { create(:omniauth_user, provider: 'ldap', password_expires_at: 2.minutes.ago) }
it 'calls the action normally' do
get :archive, params: { namespace_id: project.namespace, project_id: project, id: 'master', token: user.static_object_token }, format: 'zip'
expect(response).to have_gitlab_http_status(:ok)
end
end
end
end end
context 'when token is incorrect' do context 'when token is incorrect' do
...@@ -197,6 +220,31 @@ RSpec.describe Projects::RepositoriesController do ...@@ -197,6 +220,31 @@ RSpec.describe Projects::RepositoriesController do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
end end
context 'when user with expired password' do
let_it_be(:user) { create(:user, password_expires_at: 2.minutes.ago) }
it 'redirects to sign in page' do
request.headers['X-Gitlab-Static-Object-Token'] = user.static_object_token
get :archive, params: { namespace_id: project.namespace, project_id: project, id: 'master' }, format: 'zip'
expect(response).to have_gitlab_http_status(:found)
expect(response.location).to end_with('/users/sign_in')
end
end
context 'when password expiration is not applicable' do
context 'when ldap user' do
let_it_be(:user) { create(:omniauth_user, provider: 'ldap', password_expires_at: 2.minutes.ago) }
it 'calls the action normally' do
request.headers['X-Gitlab-Static-Object-Token'] = user.static_object_token
get :archive, params: { namespace_id: project.namespace, project_id: project, id: 'master' }, format: 'zip'
expect(response).to have_gitlab_http_status(:ok)
end
end
end
end end
context 'when token is incorrect' do context 'when token is incorrect' do
......
...@@ -117,28 +117,6 @@ RSpec.describe Projects::TagsController do ...@@ -117,28 +117,6 @@ RSpec.describe Projects::TagsController do
end end
end end
context 'private project with token authentication' do
let(:private_project) { create(:project, :repository, :private) }
it_behaves_like 'authenticates sessionless user', :index, :atom, ignore_incrementing: true do
before do
default_params.merge!(project_id: private_project, namespace_id: private_project.namespace)
private_project.add_maintainer(user)
end
end
end
context 'public project with token authentication' do
let(:public_project) { create(:project, :repository, :public) }
it_behaves_like 'authenticates sessionless user', :index, :atom, public: true do
before do
default_params.merge!(project_id: public_project, namespace_id: public_project.namespace)
end
end
end
describe 'POST #create' do describe 'POST #create' do
before do before do
project.add_developer(user) project.add_developer(user)
......
...@@ -1568,28 +1568,6 @@ RSpec.describe ProjectsController do ...@@ -1568,28 +1568,6 @@ RSpec.describe ProjectsController do
end end
end end
context 'private project with token authentication' do
let_it_be(:private_project) { create(:project, :private) }
it_behaves_like 'authenticates sessionless user', :show, :atom, ignore_incrementing: true do
before do
default_params.merge!(id: private_project, namespace_id: private_project.namespace)
private_project.add_maintainer(user)
end
end
end
context 'public project with token authentication' do
let_it_be(:public_project) { create(:project, :public) }
it_behaves_like 'authenticates sessionless user', :show, :atom, public: true do
before do
default_params.merge!(id: public_project, namespace_id: public_project.namespace)
end
end
end
context 'GET show.atom' do context 'GET show.atom' do
let_it_be(:public_project) { create(:project, :public) } let_it_be(:public_project) { create(:project, :public) }
let_it_be(:event) { create(:event, :commented, project: public_project, target: create(:note, project: public_project)) } let_it_be(:event) { create(:event, :commented, project: public_project, target: create(:note, project: public_project)) }
......
...@@ -97,6 +97,18 @@ describe('gl_emoji', () => { ...@@ -97,6 +97,18 @@ describe('gl_emoji', () => {
}); });
}); });
it('escapes gl-emoji name', async () => {
const glEmojiElement = markupToDomElement(
"<gl-emoji data-name='&#34;x=&#34y&#34 onload=&#34;alert(document.location.href)&#34;' data-unicode-version='x'>abc</gl-emoji>",
);
await waitForPromises();
expect(glEmojiElement.outerHTML).toBe(
'<gl-emoji data-name="&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;" data-unicode-version="x"><img class="emoji" title=":&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;:" alt=":&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;:" src="/-/emojis/2/grey_question.png" width="20" height="20" align="absmiddle"></gl-emoji>',
);
});
it('Adds sprite CSS if emojis are not supported', async () => { it('Adds sprite CSS if emojis are not supported', async () => {
const testPath = '/test-path.css'; const testPath = '/test-path.css';
jest.spyOn(EmojiUnicodeSupport, 'default').mockReturnValue(false); jest.spyOn(EmojiUnicodeSupport, 'default').mockReturnValue(false);
......
...@@ -560,13 +560,13 @@ RSpec.describe Resolvers::IssuesResolver do ...@@ -560,13 +560,13 @@ RSpec.describe Resolvers::IssuesResolver do
end end
it 'finds a specific issue with iid', :request_store do it 'finds a specific issue with iid', :request_store do
result = batch_sync(max_queries: 5) { resolve_issues(iid: issue1.iid).to_a } result = batch_sync(max_queries: 6) { resolve_issues(iid: issue1.iid).to_a }
expect(result).to contain_exactly(issue1) expect(result).to contain_exactly(issue1)
end end
it 'batches queries that only include IIDs', :request_store do it 'batches queries that only include IIDs', :request_store do
result = batch_sync(max_queries: 5) do result = batch_sync(max_queries: 6) do
[issue1, issue2] [issue1, issue2]
.map { |issue| resolve_issues(iid: issue.iid.to_s) } .map { |issue| resolve_issues(iid: issue.iid.to_s) }
.flat_map(&:to_a) .flat_map(&:to_a)
...@@ -576,7 +576,7 @@ RSpec.describe Resolvers::IssuesResolver do ...@@ -576,7 +576,7 @@ RSpec.describe Resolvers::IssuesResolver do
end end
it 'finds a specific issue with iids', :request_store do it 'finds a specific issue with iids', :request_store do
result = batch_sync(max_queries: 5) do result = batch_sync(max_queries: 6) do
resolve_issues(iids: [issue1.iid]).to_a resolve_issues(iids: [issue1.iid]).to_a
end end
......
...@@ -4,6 +4,12 @@ require 'spec_helper' ...@@ -4,6 +4,12 @@ require 'spec_helper'
RSpec.describe 'ActionCableSubscriptionAdapterIdentifier override' do RSpec.describe 'ActionCableSubscriptionAdapterIdentifier override' do
describe '#identifier' do describe '#identifier' do
let!(:original_config) { ::ActionCable::Server::Base.config.cable }
after do
::ActionCable::Server::Base.config.cable = original_config
end
context 'when id key is nil on cable.yml' do context 'when id key is nil on cable.yml' do
it 'does not override server config id with action cable pid' do it 'does not override server config id with action cable pid' do
config = { config = {
......
...@@ -70,8 +70,10 @@ RSpec.describe BulkImports::Common::Pipelines::UploadsPipeline do ...@@ -70,8 +70,10 @@ RSpec.describe BulkImports::Common::Pipelines::UploadsPipeline do
describe '#extract' do describe '#extract' do
it 'downloads & extracts upload paths' do it 'downloads & extracts upload paths' do
allow(Dir).to receive(:mktmpdir).and_return(tmpdir) allow(Dir).to receive(:mktmpdir).and_return(tmpdir)
expect(pipeline).to receive(:untar_zxf)
file_download_service = instance_double("BulkImports::FileDownloadService") download_service = instance_double("BulkImports::FileDownloadService")
decompression_service = instance_double("BulkImports::FileDecompressionService")
extraction_service = instance_double("BulkImports::ArchiveExtractionService")
expect(BulkImports::FileDownloadService) expect(BulkImports::FileDownloadService)
.to receive(:new) .to receive(:new)
...@@ -80,9 +82,13 @@ RSpec.describe BulkImports::Common::Pipelines::UploadsPipeline do ...@@ -80,9 +82,13 @@ RSpec.describe BulkImports::Common::Pipelines::UploadsPipeline do
relative_url: "/#{entity.pluralized_name}/test/export_relations/download?relation=uploads", relative_url: "/#{entity.pluralized_name}/test/export_relations/download?relation=uploads",
dir: tmpdir, dir: tmpdir,
filename: 'uploads.tar.gz') filename: 'uploads.tar.gz')
.and_return(file_download_service) .and_return(download_service)
expect(BulkImports::FileDecompressionService).to receive(:new).with(dir: tmpdir, filename: 'uploads.tar.gz').and_return(decompression_service)
expect(BulkImports::ArchiveExtractionService).to receive(:new).with(tmpdir: tmpdir, filename: 'uploads.tar').and_return(extraction_service)
expect(file_download_service).to receive(:execute) expect(download_service).to receive(:execute)
expect(decompression_service).to receive(:execute)
expect(extraction_service).to receive(:execute)
extracted_data = pipeline.extract(context) extracted_data = pipeline.extract(context)
...@@ -106,6 +112,16 @@ RSpec.describe BulkImports::Common::Pipelines::UploadsPipeline do ...@@ -106,6 +112,16 @@ RSpec.describe BulkImports::Common::Pipelines::UploadsPipeline do
expect { pipeline.load(context, uploads_dir_path) }.not_to change { portable.uploads.count } expect { pipeline.load(context, uploads_dir_path) }.not_to change { portable.uploads.count }
end end
end end
context 'when path is a symlink' do
it 'does not upload the file' do
symlink = File.join(tmpdir, 'symlink')
FileUtils.ln_s(File.join(tmpdir, upload_file_path), symlink)
expect { pipeline.load(context, symlink) }.not_to change { portable.uploads.count }
end
end
end end
end end
......
...@@ -101,4 +101,39 @@ RSpec.describe Gitlab::ImportExport::CommandLineUtil do ...@@ -101,4 +101,39 @@ RSpec.describe Gitlab::ImportExport::CommandLineUtil do
end end
end end
end end
describe '#untar_xf' do
let(:archive_dir) { Dir.mktmpdir }
after do
FileUtils.remove_entry(archive_dir)
end
it 'extracts archive without decompression' do
filename = 'archive.tar.gz'
archive_file = File.join(archive_dir, 'archive.tar')
FileUtils.copy_file(archive, File.join(archive_dir, filename))
subject.gunzip(dir: archive_dir, filename: filename)
result = subject.untar_xf(archive: archive_file, dir: archive_dir)
expect(result).to eq(true)
expect(File.exist?(archive_file)).to eq(true)
expect(File.exist?(File.join(archive_dir, 'project.json'))).to eq(true)
expect(Dir.exist?(File.join(archive_dir, 'uploads'))).to eq(true)
end
context 'when something goes wrong' do
it 'raises an error' do
expect(Gitlab::Popen).to receive(:popen).and_return(['Error', 1])
klass = Class.new do
include Gitlab::ImportExport::CommandLineUtil
end.new
expect { klass.untar_xf(archive: 'test', dir: 'test') }.to raise_error(Gitlab::ImportExport::Error, 'System call failed')
end
end
end
end end
...@@ -531,24 +531,6 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do ...@@ -531,24 +531,6 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
end end
end end
end end
def stub_domain_resolv(domain, ip, port = 80, &block)
address = instance_double(Addrinfo,
ip_address: ip,
ipv4_private?: true,
ipv6_linklocal?: false,
ipv4_loopback?: false,
ipv6_loopback?: false,
ipv4?: false,
ip_port: port
)
allow(Addrinfo).to receive(:getaddrinfo).with(domain, port, any_args).and_return([address])
allow(address).to receive(:ipv6_v4mapped?).and_return(false)
yield
allow(Addrinfo).to receive(:getaddrinfo).and_call_original
end
end end
context 'when enforce_user is' do context 'when enforce_user is' do
...@@ -611,6 +593,44 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do ...@@ -611,6 +593,44 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
expect(described_class).to be_blocked_url('http://foobar.x') expect(described_class).to be_blocked_url('http://foobar.x')
end end
context 'when gitlab is running on a non-default port' do
let(:gitlab_port) { 3000 }
before do
stub_config(gitlab: { protocol: 'http', host: 'gitlab.local', port: gitlab_port })
end
it 'returns true for url targeting the wrong port' do
stub_domain_resolv('gitlab.local', '127.0.0.1') do
expect(described_class).to be_blocked_url("http://gitlab.local/foo")
end
end
it 'does not block url on gitlab port' do
stub_domain_resolv('gitlab.local', '127.0.0.1') do
expect(described_class).not_to be_blocked_url("http://gitlab.local:#{gitlab_port}/foo")
end
end
end
def stub_domain_resolv(domain, ip, port = 80, &block)
address = instance_double(Addrinfo,
ip_address: ip,
ipv4_private?: true,
ipv6_linklocal?: false,
ipv4_loopback?: false,
ipv6_loopback?: false,
ipv4?: false,
ip_port: port
)
allow(Addrinfo).to receive(:getaddrinfo).with(domain, port, any_args).and_return([address])
allow(address).to receive(:ipv6_v4mapped?).and_return(false)
yield
allow(Addrinfo).to receive(:getaddrinfo).and_call_original
end
end end
describe '#validate_hostname' do describe '#validate_hostname' do
......
...@@ -16,6 +16,8 @@ RSpec.describe Integrations::ChatMessage::AlertMessage do ...@@ -16,6 +16,8 @@ RSpec.describe Integrations::ChatMessage::AlertMessage do
}.merge(Gitlab::DataBuilder::Alert.build(alert)) }.merge(Gitlab::DataBuilder::Alert.build(alert))
end end
it_behaves_like Integrations::ChatMessage
describe '#message' do describe '#message' do
it 'returns the correct message' do it 'returns the correct message' do
expect(subject.message).to eq("Alert firing in #{args[:project_name]}") expect(subject.message).to eq("Alert firing in #{args[:project_name]}")
......
...@@ -31,4 +31,22 @@ RSpec.describe Integrations::ChatMessage::BaseMessage do ...@@ -31,4 +31,22 @@ RSpec.describe Integrations::ChatMessage::BaseMessage do
it { is_expected.to eq('Check this out https://gitlab-domain.com/uploads/Screenshot1.png. And this https://gitlab-domain.com/uploads/Screenshot2.png') } it { is_expected.to eq('Check this out https://gitlab-domain.com/uploads/Screenshot1.png. And this https://gitlab-domain.com/uploads/Screenshot2.png') }
end end
end end
describe '#strip_markup' do
using RSpec::Parameterized::TableSyntax
where(:input, :output) do
nil | nil
'' | ''
'[label](url)' | 'label(url)'
'<url|label>' | 'urllabel'
'<a href="url">label</a>' | 'a href="url"label/a'
end
with_them do
it 'returns the expected output' do
expect(base_message.send(:strip_markup, input)).to eq(output)
end
end
end
end end
...@@ -3,83 +3,79 @@ ...@@ -3,83 +3,79 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Integrations::ChatMessage::DeploymentMessage do RSpec.describe Integrations::ChatMessage::DeploymentMessage do
describe '#pretext' do subject { described_class.new(args) }
it 'returns a message with the data returned by the deployment data builder' do
environment = create(:environment, name: "myenvironment") let_it_be(:user) { create(:user, name: 'John Smith', username: 'smith') }
project = create(:project, :repository) let_it_be(:namespace) { create(:namespace, name: 'myspace') }
commit = project.commit('HEAD') let_it_be(:project) { create(:project, :repository, namespace: namespace, name: 'myproject') }
deployment = create(:deployment, status: :success, environment: environment, project: project, sha: commit.sha) let_it_be(:commit) { project.commit('HEAD') }
data = Gitlab::DataBuilder::Deployment.build(deployment, Time.current) let_it_be(:ci_build) { create(:ci_build, project: project) }
let_it_be(:environment) { create(:environment, name: 'myenvironment', project: project) }
let_it_be(:deployment) { create(:deployment, status: :success, deployable: ci_build, environment: environment, project: project, user: user, sha: commit.sha) }
let(:args) do
Gitlab::DataBuilder::Deployment.build(deployment, Time.current)
end
message = described_class.new(data) it_behaves_like Integrations::ChatMessage
expect(message.pretext).to eq("Deploy to myenvironment succeeded") describe '#pretext' do
it 'returns a message with the data returned by the deployment data builder' do
expect(subject.pretext).to eq("Deploy to myenvironment succeeded")
end end
it 'returns a message for a successful deployment' do it 'returns a message for a successful deployment' do
data = { args.merge!(
status: 'success', status: 'success',
environment: 'production' environment: 'production'
} )
message = described_class.new(data) expect(subject.pretext).to eq('Deploy to production succeeded')
expect(message.pretext).to eq('Deploy to production succeeded')
end end
it 'returns a message for a failed deployment' do it 'returns a message for a failed deployment' do
data = { args.merge!(
status: 'failed', status: 'failed',
environment: 'production' environment: 'production'
} )
message = described_class.new(data) expect(subject.pretext).to eq('Deploy to production failed')
expect(message.pretext).to eq('Deploy to production failed')
end end
it 'returns a message for a canceled deployment' do it 'returns a message for a canceled deployment' do
data = { args.merge!(
status: 'canceled', status: 'canceled',
environment: 'production' environment: 'production'
} )
message = described_class.new(data)
expect(message.pretext).to eq('Deploy to production canceled') expect(subject.pretext).to eq('Deploy to production canceled')
end end
it 'returns a message for a deployment to another environment' do it 'returns a message for a deployment to another environment' do
data = { args.merge!(
status: 'success', status: 'success',
environment: 'staging' environment: 'staging'
} )
message = described_class.new(data)
expect(message.pretext).to eq('Deploy to staging succeeded') expect(subject.pretext).to eq('Deploy to staging succeeded')
end end
it 'returns a message for a deployment with any other status' do it 'returns a message for a deployment with any other status' do
data = { args.merge!(
status: 'unknown', status: 'unknown',
environment: 'staging' environment: 'staging'
} )
message = described_class.new(data) expect(subject.pretext).to eq('Deploy to staging unknown')
expect(message.pretext).to eq('Deploy to staging unknown')
end end
it 'returns a message for a running deployment' do it 'returns a message for a running deployment' do
data = { args.merge!(
status: 'running', status: 'running',
environment: 'production' environment: 'production'
} )
message = described_class.new(data)
expect(message.pretext).to eq('Starting deploy to production') expect(subject.pretext).to eq('Starting deploy to production')
end end
end end
...@@ -108,21 +104,11 @@ RSpec.describe Integrations::ChatMessage::DeploymentMessage do ...@@ -108,21 +104,11 @@ RSpec.describe Integrations::ChatMessage::DeploymentMessage do
end end
it 'returns attachments with the data returned by the deployment data builder' do it 'returns attachments with the data returned by the deployment data builder' do
user = create(:user, name: "John Smith", username: "smith")
namespace = create(:namespace, name: "myspace")
project = create(:project, :repository, namespace: namespace, name: "myproject")
commit = project.commit('HEAD')
environment = create(:environment, name: "myenvironment", project: project)
ci_build = create(:ci_build, project: project)
deployment = create(:deployment, :success, deployable: ci_build, environment: environment, project: project, user: user, sha: commit.sha)
job_url = Gitlab::Routing.url_helpers.project_job_url(project, ci_build) job_url = Gitlab::Routing.url_helpers.project_job_url(project, ci_build)
commit_url = Gitlab::UrlBuilder.build(deployment.commit) commit_url = Gitlab::UrlBuilder.build(deployment.commit)
user_url = Gitlab::Routing.url_helpers.user_url(user) user_url = Gitlab::Routing.url_helpers.user_url(user)
data = Gitlab::DataBuilder::Deployment.build(deployment, Time.current)
message = described_class.new(data) expect(subject.attachments).to eq([{
expect(message.attachments).to eq([{
text: "[myspace/myproject](#{project.web_url}) with job [##{ci_build.id}](#{job_url}) by [John Smith (smith)](#{user_url})\n[#{deployment.short_sha}](#{commit_url}): #{commit.title}", text: "[myspace/myproject](#{project.web_url}) with job [##{ci_build.id}](#{job_url}) by [John Smith (smith)](#{user_url})\n[#{deployment.short_sha}](#{commit_url}): #{commit.title}",
color: "good" color: "good"
}]) }])
......
...@@ -28,6 +28,8 @@ RSpec.describe Integrations::ChatMessage::IssueMessage do ...@@ -28,6 +28,8 @@ RSpec.describe Integrations::ChatMessage::IssueMessage do
} }
end end
it_behaves_like Integrations::ChatMessage
context 'without markdown' do context 'without markdown' do
let(:color) { '#C95823' } let(:color) { '#C95823' }
......
...@@ -29,6 +29,8 @@ RSpec.describe Integrations::ChatMessage::MergeMessage do ...@@ -29,6 +29,8 @@ RSpec.describe Integrations::ChatMessage::MergeMessage do
} }
end end
it_behaves_like Integrations::ChatMessage
context 'without markdown' do context 'without markdown' do
let(:color) { '#345' } let(:color) { '#345' }
......
...@@ -19,6 +19,10 @@ RSpec.describe Integrations::ChatMessage::NoteMessage do ...@@ -19,6 +19,10 @@ RSpec.describe Integrations::ChatMessage::NoteMessage do
name: 'project_name', name: 'project_name',
url: 'http://somewhere.com' url: 'http://somewhere.com'
}, },
commit: {
id: '5f163b2b95e6f53cbd428f5f0b103702a52b9a23',
message: "Added a commit message\ndetails\n123\n"
},
object_attributes: { object_attributes: {
id: 10, id: 10,
note: 'comment on a commit', note: 'comment on a commit',
...@@ -28,16 +32,9 @@ RSpec.describe Integrations::ChatMessage::NoteMessage do ...@@ -28,16 +32,9 @@ RSpec.describe Integrations::ChatMessage::NoteMessage do
} }
end end
context 'commit notes' do it_behaves_like Integrations::ChatMessage
before do
args[:object_attributes][:note] = 'comment on a commit'
args[:object_attributes][:noteable_type] = 'Commit'
args[:commit] = {
id: '5f163b2b95e6f53cbd428f5f0b103702a52b9a23',
message: "Added a commit message\ndetails\n123\n"
}
end
context 'commit notes' do
context 'without markdown' do context 'without markdown' do
it 'returns a message regarding notes on commits' do it 'returns a message regarding notes on commits' do
expect(subject.pretext).to eq("Test User (test.user) <http://url.com|commented on " \ expect(subject.pretext).to eq("Test User (test.user) <http://url.com|commented on " \
......
...@@ -40,6 +40,8 @@ RSpec.describe Integrations::ChatMessage::PipelineMessage do ...@@ -40,6 +40,8 @@ RSpec.describe Integrations::ChatMessage::PipelineMessage do
let(:has_yaml_errors) { false } let(:has_yaml_errors) { false }
it_behaves_like Integrations::ChatMessage
before do before do
test_commit = double("A test commit", committer: args[:user], title: "A test commit message") test_commit = double("A test commit", committer: args[:user], title: "A test commit message")
test_project = double("A test project", commit_by: test_commit, name: args[:project][:name], web_url: args[:project][:web_url]) test_project = double("A test project", commit_by: test_commit, name: args[:project][:name], web_url: args[:project][:web_url])
......
...@@ -19,6 +19,8 @@ RSpec.describe Integrations::ChatMessage::PushMessage do ...@@ -19,6 +19,8 @@ RSpec.describe Integrations::ChatMessage::PushMessage do
let(:color) { '#345' } let(:color) { '#345' }
it_behaves_like Integrations::ChatMessage
context 'push' do context 'push' do
before do before do
args[:commits] = [ args[:commits] = [
......
...@@ -33,6 +33,8 @@ RSpec.describe Integrations::ChatMessage::WikiPageMessage do ...@@ -33,6 +33,8 @@ RSpec.describe Integrations::ChatMessage::WikiPageMessage do
} }
end end
it_behaves_like Integrations::ChatMessage
context 'without markdown' do context 'without markdown' do
describe '#pretext' do describe '#pretext' do
context 'when :action == "create"' do context 'when :action == "create"' do
......
...@@ -253,7 +253,7 @@ RSpec.describe 'GraphQL' do ...@@ -253,7 +253,7 @@ RSpec.describe 'GraphQL' do
end end
context 'with token authentication' do context 'with token authentication' do
let(:token) { create(:personal_access_token) } let(:token) { create(:personal_access_token, user: user) }
it 'authenticates users with a PAT' do it 'authenticates users with a PAT' do
stub_authentication_activity_metrics(debug: false) stub_authentication_activity_metrics(debug: false)
...@@ -276,6 +276,32 @@ RSpec.describe 'GraphQL' do ...@@ -276,6 +276,32 @@ RSpec.describe 'GraphQL' do
expect(graphql_errors).to include({ 'message' => /API not accessible/ }) expect(graphql_errors).to include({ 'message' => /API not accessible/ })
end end
context 'when user with expired password' do
let_it_be(:user) { create(:user, password_expires_at: 2.minutes.ago) }
it 'does not authenticate user' do
post_graphql(query, headers: { 'PRIVATE-TOKEN' => token.token })
expect(response).to have_gitlab_http_status(:ok)
expect(graphql_data['echo']).to eq('nil says: Hello world')
end
end
context 'when password expiration is not applicable' do
context 'when ldap user' do
let_it_be(:user) { create(:omniauth_user, provider: 'ldap', password_expires_at: 2.minutes.ago) }
it 'authenticates user' do
post_graphql(query, headers: { 'PRIVATE-TOKEN' => token.token })
expect(response).to have_gitlab_http_status(:ok)
expect(graphql_data['echo']).to eq("\"#{token.user.username}\" says: Hello world")
end
end
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])
......
...@@ -3138,6 +3138,29 @@ RSpec.describe API::Projects do ...@@ -3138,6 +3138,29 @@ RSpec.describe API::Projects do
expect(json_response['message']).to eq('404 Project Not Found') expect(json_response['message']).to eq('404 Project Not Found')
end end
it 'returns 404 if the source project members cannot be viewed by the requester' do
private_project = create(:project, :private)
expect do
post api("/projects/#{project.id}/import_project_members/#{private_project.id}", user)
end.not_to change { project.members.count }
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 Project Not Found')
end
it 'returns 403 if the source project members cannot be administered by the requester' do
project.add_maintainer(user2)
project2.add_developer(user2)
expect do
post api("/projects/#{project.id}/import_project_members/#{project2.id}", user2)
end.not_to change { project.members.count }
expect(response).to have_gitlab_http_status(:forbidden)
expect(json_response['message']).to eq('403 Forbidden - Project')
end
it 'returns 422 if the import failed for valid projects' do it 'returns 422 if the import failed for valid projects' do
allow_next_instance_of(::ProjectTeam) do |project_team| allow_next_instance_of(::ProjectTeam) do |project_team|
allow(project_team).to receive(:import).and_return(false) allow(project_team).to receive(:import).and_return(false)
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Dashboard::ProjectsController do
context 'token authentication' do
it_behaves_like 'authenticates sessionless user for the request spec', 'index atom', public_resource: false do
let(:url) { dashboard_projects_url(:atom) }
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe DashboardController do
context 'token authentication' do
it_behaves_like 'authenticates sessionless user for the request spec', 'issues atom', public_resource: false do
let(:url) { issues_dashboard_url(:atom, assignee_username: user.username) }
end
it_behaves_like 'authenticates sessionless user for the request spec', 'issues_calendar ics', public_resource: false do
let(:url) { issues_dashboard_url(:ics, assignee_username: user.username) }
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GroupsController do
context 'token authentication' do
context 'when public group' do
let_it_be(:public_group) { create(:group, :public) }
it_behaves_like 'authenticates sessionless user for the request spec', 'show atom', public_resource: true do
let(:url) { group_path(public_group, format: :atom) }
end
it_behaves_like 'authenticates sessionless user for the request spec', 'issues atom', public_resource: true do
let(:url) { issues_group_path(public_group, format: :atom) }
end
it_behaves_like 'authenticates sessionless user for the request spec', 'issues_calendar ics', public_resource: true do
let(:url) { issues_group_calendar_url(public_group, format: :ics) }
end
end
context 'when private project' do
let_it_be(:private_group) { create(:group, :private) }
it_behaves_like 'authenticates sessionless user for the request spec', 'show atom', public_resource: false, ignore_metrics: true do
let(:url) { group_path(private_group, format: :atom) }
before do
private_group.add_maintainer(user)
end
end
it_behaves_like 'authenticates sessionless user for the request spec', 'issues atom', public_resource: false, ignore_metrics: true do
let(:url) { issues_group_path(private_group, format: :atom) }
before do
private_group.add_maintainer(user)
end
end
it_behaves_like 'authenticates sessionless user for the request spec', 'issues_calendar ics', public_resource: false, ignore_metrics: true do
let(:url) { issues_group_calendar_url(private_group, format: :ics) }
before do
private_group.add_maintainer(user)
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Projects::CommitsController do
context 'token authentication' do
context 'when public project' do
let_it_be(:public_project) { create(:project, :repository, :public) }
it_behaves_like 'authenticates sessionless user for the request spec', 'show atom', public_resource: true do
let(:url) { project_commits_url(public_project, public_project.default_branch, format: :atom) }
end
end
context 'when private project' do
let_it_be(:private_project) { create(:project, :repository, :private) }
it_behaves_like 'authenticates sessionless user for the request spec', 'show atom', public_resource: false, ignore_metrics: true do
let(:url) { project_commits_url(private_project, private_project.default_branch, format: :atom) }
before do
private_project.add_maintainer(user)
end
end
end
end
end
...@@ -8,11 +8,11 @@ RSpec.describe Projects::IssuesController do ...@@ -8,11 +8,11 @@ RSpec.describe Projects::IssuesController do
let_it_be(:project) { issue.project } let_it_be(:project) { issue.project }
let_it_be(:user) { issue.author } let_it_be(:user) { issue.author }
before do
login_as(user)
end
describe 'GET #discussions' do describe 'GET #discussions' do
before do
login_as(user)
end
let_it_be(:discussion) { create(:discussion_note_on_issue, noteable: issue, project: issue.project) } let_it_be(:discussion) { create(:discussion_note_on_issue, noteable: issue, project: issue.project) }
let_it_be(:discussion_reply) { create(:discussion_note_on_issue, noteable: issue, project: issue.project, in_reply_to: discussion) } let_it_be(:discussion_reply) { create(:discussion_note_on_issue, noteable: issue, project: issue.project, in_reply_to: discussion) }
let_it_be(:state_event) { create(:resource_state_event, issue: issue) } let_it_be(:state_event) { create(:resource_state_event, issue: issue) }
...@@ -68,4 +68,38 @@ RSpec.describe Projects::IssuesController do ...@@ -68,4 +68,38 @@ RSpec.describe Projects::IssuesController do
end end
end end
end end
context 'token authentication' do
context 'when public project' do
let_it_be(:public_project) { create(:project, :public) }
it_behaves_like 'authenticates sessionless user for the request spec', 'index atom', public_resource: true do
let(:url) { project_issues_url(public_project, format: :atom) }
end
it_behaves_like 'authenticates sessionless user for the request spec', 'calendar ics', public_resource: true do
let(:url) { project_issues_url(public_project, format: :ics) }
end
end
context 'when private project' do
let_it_be(:private_project) { create(:project, :private) }
it_behaves_like 'authenticates sessionless user for the request spec', 'index atom', public_resource: false, ignore_metrics: true do
let(:url) { project_issues_url(private_project, format: :atom) }
before do
private_project.add_maintainer(user)
end
end
it_behaves_like 'authenticates sessionless user for the request spec', 'calendar ics', public_resource: false, ignore_metrics: true do
let(:url) { project_issues_url(private_project, format: :ics) }
before do
private_project.add_maintainer(user)
end
end
end
end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Projects::MergeRequestsController do
context 'token authentication' do
context 'when public project' do
let_it_be(:public_project) { create(:project, :public) }
it_behaves_like 'authenticates sessionless user for the request spec', 'index atom', public_resource: true do
let(:url) { project_merge_requests_url(public_project, format: :atom) }
end
end
context 'when private project' do
let_it_be(:private_project) { create(:project, :private) }
it_behaves_like 'authenticates sessionless user for the request spec', 'index atom', public_resource: false, ignore_metrics: true do
let(:url) { project_merge_requests_url(private_project, format: :atom) }
before do
private_project.add_maintainer(user)
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Projects::TagsController do
context 'token authentication' do
context 'when public project' do
let_it_be(:public_project) { create(:project, :repository, :public) }
it_behaves_like 'authenticates sessionless user for the request spec', 'index atom', public_resource: true do
let(:url) { project_tags_url(public_project, format: :atom) }
end
end
context 'when private project' do
let_it_be(:private_project) { create(:project, :repository, :private) }
it_behaves_like 'authenticates sessionless user for the request spec', 'index atom', public_resource: false, ignore_metrics: true do
let(:url) { project_tags_url(private_project, format: :atom) }
before do
private_project.add_maintainer(user)
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ProjectsController do
context 'token authentication' do
context 'when public project' do
let_it_be(:public_project) { create(:project, :public) }
it_behaves_like 'authenticates sessionless user for the request spec', 'show atom', public_resource: true do
let(:url) { project_url(public_project, format: :atom) }
end
end
context 'when private project' do
let_it_be(:private_project) { create(:project, :private) }
it_behaves_like 'authenticates sessionless user for the request spec', 'show atom', public_resource: false, ignore_metrics: true do
let(:url) { project_url(private_project, format: :atom) }
before do
private_project.add_maintainer(user)
end
end
end
end
end
...@@ -805,9 +805,9 @@ RSpec.describe UsersController do ...@@ -805,9 +805,9 @@ RSpec.describe UsersController do
end end
context 'token authentication' do context 'token authentication' do
let(:url) { user_url(user.username, format: :atom) } it_behaves_like 'authenticates sessionless user for the request spec', 'show atom', public_resource: true do
let(:url) { user_url(user, format: :atom) }
it_behaves_like 'authenticates sessionless user for the request spec', public: true end
end end
def user_moved_message(redirect_route, user) def user_moved_message(redirect_route, user)
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe BulkImports::ArchiveExtractionService do
let_it_be(:tmpdir) { Dir.mktmpdir }
let_it_be(:filename) { 'symlink_export.tar' }
let_it_be(:filepath) { File.join(tmpdir, filename) }
before do
FileUtils.copy_file(File.join('spec', 'fixtures', filename), filepath)
end
after(:all) do
FileUtils.remove_entry(tmpdir)
end
subject(:service) { described_class.new(tmpdir: tmpdir, filename: filename) }
describe '#execute' do
it 'extracts files from archive and removes symlinks' do
file = File.join(tmpdir, 'project.json')
folder = File.join(tmpdir, 'uploads')
symlink = File.join(tmpdir, 'uploads', 'link.gitignore')
expect(service).to receive(:untar_xf).with(archive: filepath, dir: tmpdir).and_call_original
service.execute
expect(File.exist?(file)).to eq(true)
expect(Dir.exist?(folder)).to eq(true)
expect(File.exist?(symlink)).to eq(false)
end
context 'when dir is not in tmpdir' do
it 'raises an error' do
['/etc', '/usr', '/', '/home', '', '/some/other/path', Rails.root].each do |path|
expect { described_class.new(tmpdir: path, filename: 'filename').execute }
.to raise_error(BulkImports::Error, 'Invalid target directory')
end
end
end
context 'when archive file is a symlink' do
it 'raises an error' do
FileUtils.ln_s(File.join(tmpdir, filename), File.join(tmpdir, 'symlink'))
expect { described_class.new(tmpdir: tmpdir, filename: 'symlink').execute }
.to raise_error(BulkImports::Error, 'Invalid file')
end
end
context 'when filepath is being traversed' do
it 'raises an error' do
expect { described_class.new(tmpdir: File.join(tmpdir, '../../../'), filename: 'name').execute }
.to raise_error(Gitlab::Utils::PathTraversalAttackError, 'Invalid path')
end
end
end
end
...@@ -11,10 +11,8 @@ RSpec.describe Packages::Npm::CreatePackageService do ...@@ -11,10 +11,8 @@ RSpec.describe Packages::Npm::CreatePackageService do
Gitlab::Json.parse(fixture_file('packages/npm/payload.json') Gitlab::Json.parse(fixture_file('packages/npm/payload.json')
.gsub('@root/npm-test', package_name) .gsub('@root/npm-test', package_name)
.gsub('1.0.1', version)).with_indifferent_access .gsub('1.0.1', version)).with_indifferent_access
.merge!(override)
end end
let(:override) { {} }
let(:package_name) { "@#{namespace.path}/my-app" } let(:package_name) { "@#{namespace.path}/my-app" }
let(:version_data) { params.dig('versions', '1.0.1') } let(:version_data) { params.dig('versions', '1.0.1') }
...@@ -116,13 +114,53 @@ RSpec.describe Packages::Npm::CreatePackageService do ...@@ -116,13 +114,53 @@ RSpec.describe Packages::Npm::CreatePackageService do
it { expect(subject[:message]).to be 'Package already exists.' } it { expect(subject[:message]).to be 'Package already exists.' }
end end
context 'file size above maximum limit' do describe 'max file size validation' do
let(:max_file_size) { 5.bytes}
shared_examples_for 'max file size validation failure' do
it 'returns a 400 error', :aggregate_failures do
expect(subject[:http_status]).to eq 400
expect(subject[:message]).to be 'File is too large.'
end
end
before do before do
params['_attachments']["#{package_name}-#{version}.tgz"]['length'] = project.actual_limits.npm_max_file_size + 1 project.actual_limits.update!(npm_max_file_size: max_file_size)
end end
it { expect(subject[:http_status]).to eq 400 } context 'when max file size is exceeded' do
it { expect(subject[:message]).to be 'File is too large.' } # NOTE: The base64 encoded package data in the fixture file is the "hello\n" string, whose byte size is 6.
it_behaves_like 'max file size validation failure'
end
context 'when file size is faked by setting the attachment length param to a lower size' do
let(:params) { super().deep_merge!( { _attachments: { "#{package_name}-#{version}.tgz" => { data: encoded_package_data, length: 1 } } }) }
# TODO (technical debt): Extract the package size calculation outside the service and add separate specs for it.
# Right now we have several contexts here to test the calculation's different scenarios.
context "when encoded package data is not padded" do
# 'Hello!' (size = 6 bytes) => 'SGVsbG8h'
let(:encoded_package_data) { 'SGVsbG8h' }
it_behaves_like 'max file size validation failure'
end
context "when encoded package data is padded with '='" do
let(:max_file_size) { 4.bytes}
# 'Hello' (size = 5 bytes) => 'SGVsbG8='
let(:encoded_package_data) { 'SGVsbG8=' }
it_behaves_like 'max file size validation failure'
end
context "when encoded package data is padded with '=='" do
let(:max_file_size) { 3.bytes}
# 'Hell' (size = 4 bytes) => 'SGVsbA=='
let(:encoded_package_data) { 'SGVsbA==' }
it_behaves_like 'max file size validation failure'
end
end
end end
[ [
...@@ -141,7 +179,7 @@ RSpec.describe Packages::Npm::CreatePackageService do ...@@ -141,7 +179,7 @@ RSpec.describe Packages::Npm::CreatePackageService do
end end
context 'with empty versions' do context 'with empty versions' do
let(:override) { { versions: {} } } let(:params) { super().merge!({ versions: {} } ) }
it { expect(subject[:http_status]).to eq 400 } it { expect(subject[:http_status]).to eq 400 }
it { expect(subject[:message]).to eq 'Version is empty.' } it { expect(subject[:message]).to eq 'Version is empty.' }
......
# frozen_string_literal: true
# This controller shared examples will be migrated to
# spec/support/shared_examples/requests/sessionless_auth_request_shared_examples.rb
# See also https://gitlab.com/groups/gitlab-org/-/epics/5076
RSpec.shared_examples 'authenticates sessionless user' do |path, format, params|
params ||= {}
before do
stub_authentication_activity_metrics(debug: false)
end
let(:user) { create(:user) }
let(:personal_access_token) { create(:personal_access_token, user: user) }
let(:default_params) { { format: format }.merge(params.except(:public) || {}) }
context "when the 'personal_access_token' param is populated with the personal access token" do
it 'logs the user in' do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
.and increment(:user_session_override_counter)
.and increment(:user_sessionless_authentication_counter)
get path, params: default_params.merge(private_token: personal_access_token.token)
expect(response).to have_gitlab_http_status(:ok)
expect(controller.current_user).to eq(user)
end
it 'does not log the user in if page is public', if: params[:public] do
get path, params: default_params
expect(response).to have_gitlab_http_status(:ok)
expect(controller.current_user).to be_nil
end
end
context 'when the personal access token has no api scope', unless: params[:public] do
it 'does not log the user in' do
# Several instances of where these specs are shared route the request
# through ApplicationController#route_not_found which does not involve
# the usual auth code from Devise, so does not increment the
# :user_unauthenticated_counter
#
unless params[:ignore_incrementing]
expect(authentication_metrics)
.to increment(:user_unauthenticated_counter)
end
personal_access_token.update!(scopes: [:read_user])
get path, params: default_params.merge(private_token: personal_access_token.token)
expect(response).not_to have_gitlab_http_status(:ok)
end
end
context "when the 'PERSONAL_ACCESS_TOKEN' header is populated with the personal access token" do
it 'logs the user in' do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
.and increment(:user_session_override_counter)
.and increment(:user_sessionless_authentication_counter)
@request.headers['PRIVATE-TOKEN'] = personal_access_token.token
get path, params: default_params
expect(response).to have_gitlab_http_status(:ok)
end
end
context "when the 'feed_token' param is populated with the feed token", if: format == :rss do
it "logs the user in" do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
.and increment(:user_session_override_counter)
.and increment(:user_sessionless_authentication_counter)
get path, params: default_params.merge(feed_token: user.feed_token)
expect(response).to have_gitlab_http_status(:ok)
end
end
context "when the 'feed_token' param is populated with an invalid feed token", if: format == :rss, unless: params[:public] do
it "logs the user" do
expect(authentication_metrics)
.to increment(:user_unauthenticated_counter)
get path, params: default_params.merge(feed_token: 'token')
expect(response).not_to have_gitlab_http_status(:ok)
end
end
it "doesn't log the user in otherwise", unless: params[:public] do
# Several instances of where these specs are shared route the request
# through ApplicationController#route_not_found which does not involve
# the usual auth code from Devise, so does not increment the
# :user_unauthenticated_counter
#
unless params[:ignore_incrementing]
expect(authentication_metrics)
.to increment(:user_unauthenticated_counter)
end
get path, params: default_params.merge(private_token: 'token')
expect(response).not_to have_gitlab_http_status(:ok)
end
end
# frozen_string_literal: true
RSpec.shared_examples Integrations::ChatMessage do
context 'when input contains link markup' do
let(:evil_input) { '[Markdown](http://evil.com) <a href="http://evil.com">HTML</a> <http://evil.com|Slack>' }
# Attributes returned from #activity and #attributes which should be sanitized.
let(:sanitized_attributes) do
%i[title subtitle text fallback author_name]
end
# Attributes passed to #initialize which can contain user input.
before do
args.deep_merge!(
project_name: evil_input,
user_name: evil_input,
user_full_name: evil_input,
commit_title: evil_input,
environment: evil_input,
project: {
name: evil_input
},
user: {
name: evil_input,
username: evil_input
},
object_attributes: {
title: evil_input
}
)
end
# NOTE: The `include` matcher is used here so the RSpec error messages will tell us
# which method or attribute is failing, even though it makes the spec a bit less readable.
it 'strips all link markup characters', :aggregate_failures do
expect(subject).not_to have_attributes(
pretext: include(evil_input),
summary: include(evil_input)
)
begin
sanitized_attributes.each do |attribute|
expect(subject.activity).not_to include(attribute => include(evil_input))
end
rescue NotImplementedError
end
begin
sanitized_attributes.each do |attribute|
expect(subject.attachments).not_to include(include(attribute => include(evil_input)))
end
rescue NotImplementedError
end
end
end
end
# frozen_string_literal: true # frozen_string_literal: true
RSpec.shared_examples 'authenticates sessionless user for the request spec' do |params| RSpec.shared_examples 'authenticates sessionless user for the request spec' do |name, public_resource:, ignore_metrics: false, params: {}|
params ||= {}
before do before do
stub_authentication_activity_metrics(debug: false) stub_authentication_activity_metrics(debug: false)
end end
let(:user) { create(:user) } let_it_be(:user) { create(:user) }
let(:personal_access_token) { create(:personal_access_token, user: user) } let(:personal_access_token) { create(:personal_access_token, user: user) }
let(:default_params) { params.except(:public) || {} }
context "when the 'personal_access_token' param is populated with the personal access token" do shared_examples 'authenticates user and returns response with ok status' do
it 'logs the user in' do it 'authenticates user and returns response with ok status' do
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)
.and increment(:user_sessionless_authentication_counter) .and increment(:user_sessionless_authentication_counter)
get url, params: default_params.merge(private_token: personal_access_token.token) subject
expect(response).to have_gitlab_http_status(:ok)
expect(controller.current_user).to eq(user) expect(controller.current_user).to eq(user)
expect(response).to have_gitlab_http_status(:ok)
end end
end
it 'does not log the user in if page is public', if: params[:public] do shared_examples 'does not authenticate user and returns response with ok status' do
get url, params: default_params it 'does not authenticate user and returns response with ok status' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(controller.current_user).to be_nil expect(controller.current_user).to be_nil
expect(response).to have_gitlab_http_status(:ok)
end end
end end
context 'when the personal access token has no api scope', unless: params[:public] do shared_examples 'does not return response with ok status' do
it 'does not log the user in' do it 'does not return response with ok status' do
# Several instances of where these specs are shared route the request # Several instances of where these specs are shared route the request
# through ApplicationController#route_not_found which does not involve # through ApplicationController#route_not_found which does not involve
# the usual auth code from Devise, so does not increment the # the usual auth code from Devise, so does not increment the
# :user_unauthenticated_counter # :user_unauthenticated_counter
# unless ignore_metrics
unless params[:ignore_incrementing]
expect(authentication_metrics) expect(authentication_metrics)
.to increment(:user_unauthenticated_counter) .to increment(:user_unauthenticated_counter)
end end
personal_access_token.update!(scopes: [:read_user]) subject
get url, params: default_params.merge(private_token: personal_access_token.token)
expect(response).not_to have_gitlab_http_status(:ok) expect(response).not_to have_gitlab_http_status(:ok)
end end
end end
context "when the 'PERSONAL_ACCESS_TOKEN' header is populated with the personal access token" do shared_examples 'using valid token' do
it 'logs the user in' do context 'when resource is private', unless: public_resource do
expect(authentication_metrics) include_examples 'authenticates user and returns response with ok status'
.to increment(:user_authenticated_counter)
.and increment(:user_session_override_counter)
.and increment(:user_sessionless_authentication_counter)
headers = { 'PRIVATE-TOKEN': personal_access_token.token } context 'when user with expired password' do
get url, params: default_params, headers: headers let_it_be(:user) { create(:user, password_expires_at: 2.minutes.ago) }
expect(response).to have_gitlab_http_status(:ok) include_examples 'does not return response with ok status'
end
context 'when password expiration is not applicable' do
context 'when ldap user' do
let_it_be(:user) { create(:omniauth_user, provider: 'ldap', password_expires_at: 2.minutes.ago) }
include_examples 'authenticates user and returns response with ok status'
end
end
end
context 'when resource is public', if: public_resource do
include_examples 'authenticates user and returns response with ok status'
context 'when user with expired password' do
let_it_be(:user) { create(:user, password_expires_at: 2.minutes.ago) }
include_examples 'does not authenticate user and returns response with ok status'
end
end end
end end
it "doesn't log the user in otherwise", unless: params[:public] do shared_examples 'using invalid token' do
# Several instances of where these specs are shared route the request context 'when resource is private', unless: public_resource do
# through ApplicationController#route_not_found which does not involve include_examples 'does not return response with ok status'
# the usual auth code from Devise, so does not increment the end
# :user_unauthenticated_counter
# context 'when resource is public', if: public_resource do
unless params[:ignore_incrementing] include_examples 'does not authenticate user and returns response with ok status'
expect(authentication_metrics) end
.to increment(:user_unauthenticated_counter) end
shared_examples 'personal access token has no api scope' do
context 'when the personal access token has no api scope' do
before do
personal_access_token.update!(scopes: [:read_user])
end
context 'when resource is private', unless: public_resource do
include_examples 'does not return response with ok status'
end
context 'when resource is public', if: public_resource do
include_examples 'does not authenticate user and returns response with ok status'
end
end
end
describe name do
context "when the 'private_token' param is populated with the personal access token" do
context 'when valid token' do
subject { get url, params: params.merge(private_token: personal_access_token.token) }
include_examples 'using valid token'
include_examples 'personal access token has no api scope'
end
context 'when invalid token' do
subject { get url, params: params.merge(private_token: 'invalid token') }
include_examples 'using invalid token'
end
end end
get url, params: default_params.merge(private_token: 'token') context "when the 'PRIVATE-TOKEN' header is populated with the personal access token" do
context 'when valid token' do
subject do
headers = { 'PRIVATE-TOKEN': personal_access_token.token }
get url, params: params, headers: headers
end
expect(response).not_to have_gitlab_http_status(:ok) include_examples 'using valid token'
include_examples 'personal access token has no api scope'
end
context 'when invalid token' do
subject do
headers = { 'PRIVATE-TOKEN': 'invalid token' }
get url, params: params, headers: headers
end
include_examples 'using invalid token'
end
end
context "when the 'feed_token' param is populated with the feed token" do
context 'when valid token' do
subject { get url, params: params.merge(feed_token: user.feed_token) }
include_examples 'using valid token'
end
context 'when invalid token' do
subject { get url, params: params.merge(feed_token: 'invalid token') }
include_examples 'using invalid token'
end
end
end end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment