Commit 3e8fc446 authored by GitLab Release Tools Bot's avatar GitLab Release Tools Bot

Merge remote-tracking branch 'dev/master'

parents 0716b8a1 c704edad
......@@ -2,6 +2,10 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 14.6.2 (2022-01-10)
No changes.
## 14.6.1 (2022-01-04)
### Fixed (2 changes)
......@@ -387,6 +391,10 @@ entry.
- [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))
## 14.5.3 (2022-01-11)
No changes.
## 14.5.2 (2021-12-03)
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))
- [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)
No changes.
3627c14a64ce48446e8a67299c3161ff7290d1ad
e02b0d67e48ed5a4493b073c9836d376a780f34d
......@@ -64,10 +64,12 @@ class GlEmoji extends HTMLElement {
this.classList.add('emoji-icon');
this.classList.add(fallbackSpriteClass);
} else if (hasImageFallback) {
this.innerHTML = emojiImageTag(name, fallbackSrc);
this.innerHTML = '';
this.appendChild(emojiImageTag(name, fallbackSrc));
} else {
const src = emojiFallbackImageSrc(name);
this.innerHTML = emojiImageTag(name, src);
this.innerHTML = '';
this.appendChild(emojiImageTag(name, src));
}
}
});
......
import { escape, minBy } from 'lodash';
import emojiRegexFactory from 'emoji-regex';
import emojiAliases from 'emojis/aliases.json';
import { setAttributes } from '~/lib/utils/dom_utils';
import AccessorUtilities from '../lib/utils/accessor';
import axios from '../lib/utils/axios_utils';
import { CACHE_KEY, CACHE_VERSION_KEY, CATEGORY_ICON_MAP, FREQUENTLY_USED_KEY } from './constants';
......@@ -220,7 +221,19 @@ export function emojiFallbackImageSrc(inputName) {
}
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) {
......
......@@ -20,7 +20,7 @@ module SessionlessAuthentication
end
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
# actually stored in the session and a token is needed
# for every request. If you want the token to work as a
......
......@@ -28,8 +28,14 @@ class Import::GithubController < Import::BaseController
end
def callback
session[access_token_key] = get_token(params[:code])
redirect_to status_import_url
auth_state = session[auth_state_key]
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
def personal_access_token
......@@ -154,13 +160,16 @@ class Import::GithubController < Import::BaseController
end
def authorize_url
state = SecureRandom.base64(64)
session[auth_state_key] = state
if Feature.enabled?(:remove_legacy_github_client)
oauth_client.auth_code.authorize_url(
redirect_uri: callback_import_url,
scope: 'repo, user, user:email'
scope: 'repo, user, user:email',
state: state
)
else
client.authorize_url(callback_import_url)
client.authorize_url(callback_import_url, state)
end
end
......@@ -219,6 +228,10 @@ class Import::GithubController < Import::BaseController
alert: _('Missing OAuth configuration for GitHub.')
end
def auth_state_key
:"#{provider_name}_auth_state_key"
end
def access_token_key
:"#{provider_name}_access_token"
end
......
......@@ -36,7 +36,7 @@ module Resolvers
def unconditional_includes
[
{
project: [:project_feature]
project: [:project_feature, :group]
},
:author
]
......
......@@ -6,6 +6,13 @@ module Integrations
def notify(message, opts)
# 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.ping(
message.pretext,
......
......@@ -23,7 +23,7 @@ module Integrations
def attachments
[{
title: title,
title: strip_markup(title),
title_link: alert_url,
color: attachment_color,
fields: attachment_fields
......@@ -31,7 +31,7 @@ module Integrations
end
def message
"Alert firing in #{project_name}"
"Alert firing in #{strip_markup(project_name)}"
end
private
......
......@@ -5,6 +5,10 @@ module Integrations
class BaseMessage
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 :user_full_name
attr_reader :user_name
......@@ -65,12 +69,26 @@ module Integrations
string.gsub(RELATIVE_LINK_REGEX, "#{project_url}\\1")
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
'#345'
end
def link(text, url)
"[#{text}](#{url})"
"[#{strip_markup(text)}](#{url})"
end
def pretty_duration(seconds)
......
......@@ -27,7 +27,7 @@ module Integrations
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
}]
end
......@@ -40,9 +40,9 @@ module Integrations
def message
if running?
"Starting deploy to #{environment}"
"Starting deploy to #{strip_markup(environment)}"
else
"Deploy to #{environment} #{humanized_status}"
"Deploy to #{strip_markup(environment)} #{humanized_status}"
end
end
......
......@@ -32,7 +32,7 @@ module Integrations
def activity
{
title: "Issue #{state} by #{user_combined_name}",
title: "Issue #{state} by #{strip_markup(user_combined_name)}",
subtitle: "in #{project_link}",
text: issue_link,
image: user_avatar
......@@ -42,7 +42,7 @@ module Integrations
private
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
def opened_issue?
......@@ -67,7 +67,7 @@ module Integrations
end
def issue_title
"#{Issue.reference_prefix}#{issue_iid} #{title}"
"#{Issue.reference_prefix}#{issue_iid} #{strip_markup(title)}"
end
end
end
......
......@@ -29,7 +29,7 @@ module Integrations
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}",
text: merge_request_link,
image: user_avatar
......@@ -39,7 +39,7 @@ module Integrations
private
def format_title(title)
'*' + title.lines.first.chomp + '*'
'*' + strip_markup(title.lines.first.chomp) + '*'
end
def message
......@@ -51,7 +51,7 @@ module Integrations
end
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
def merge_request_link
......@@ -59,7 +59,7 @@ module Integrations
end
def merge_request_title
"#{MergeRequest.reference_prefix}#{merge_request_iid} #{title}"
"#{MergeRequest.reference_prefix}#{merge_request_iid} #{strip_markup(title)}"
end
def merge_request_url
......
......@@ -35,9 +35,9 @@ module Integrations
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}",
text: formatted_title,
text: strip_markup(formatted_title),
image: user_avatar
}
end
......@@ -45,7 +45,7 @@ module Integrations
private
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
def format_title(title)
......
......@@ -56,7 +56,7 @@ module Integrations
[{
fallback: format(message),
color: attachment_color,
author_name: user_combined_name,
author_name: strip_markup(user_combined_name),
author_icon: user_avatar,
author_link: author_url,
title: s_("ChatMessage|Pipeline #%{pipeline_id} %{humanized_status} in %{duration}") %
......@@ -80,7 +80,7 @@ module Integrations
pipeline_link: pipeline_link,
ref_type: ref_type,
ref_link: ref_link,
user_combined_name: user_combined_name,
user_combined_name: strip_markup(user_combined_name),
humanized_status: humanized_status
},
subtitle: s_("ChatMessage|in %{project_link}") % { project_link: project_link },
......@@ -154,7 +154,7 @@ module Integrations
pipeline_link: pipeline_link,
ref_type: ref_type,
ref_link: ref_link,
user_combined_name: user_combined_name,
user_combined_name: strip_markup(user_combined_name),
humanized_status: humanized_status,
duration: pretty_duration(duration)
}
......@@ -189,7 +189,7 @@ module Integrations
end
def ref_link
"[#{ref}](#{ref_url})"
link(ref, ref_url)
end
def project_url
......@@ -197,7 +197,7 @@ module Integrations
end
def project_link
"[#{project.name}](#{project_url})"
link(project.name, project_url)
end
def pipeline_failed_jobs_url
......@@ -213,7 +213,7 @@ module Integrations
end
def pipeline_link
"[##{pipeline_id}](#{pipeline_url})"
link("##{pipeline_id}", pipeline_url)
end
def job_url(job)
......@@ -221,7 +221,7 @@ module Integrations
end
def job_link(job)
"[#{job[:name]}](#{job_url(job)})"
link(job[:name], job_url(job))
end
def failed_jobs_links
......@@ -242,7 +242,7 @@ module Integrations
def stage_link(stage)
# All stages link to the pipeline page
"[#{stage}](#{pipeline_url})"
link(stage, pipeline_url)
end
def failed_stages_links
......@@ -254,7 +254,7 @@ module Integrations
end
def commit_link
"[#{commit.title}](#{commit_url})"
link(commit.title, commit_url)
end
def author_url
......
......@@ -39,7 +39,7 @@ module Integrations
def humanized_action(short: false)
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.join(' ')
end
......@@ -67,7 +67,7 @@ module Integrations
url = commit[:url]
"[#{id}](#{url}): #{title} - #{author}"
"#{link(id, url)}: #{strip_markup(title)} - #{strip_markup(author)}"
end
def new_branch?
......@@ -91,15 +91,15 @@ module Integrations
end
def ref_link
"[#{ref}](#{ref_url})"
link(ref, ref_url)
end
def project_link
"[#{project_name}](#{project_url})"
link(project_name, project_url)
end
def compare_link
"[Compare changes](#{compare_url})"
link('Compare changes', compare_url)
end
def compose_action_details
......
......@@ -36,9 +36,9 @@ module Integrations
def activity
{
title: "#{user_combined_name} #{action} #{wiki_page_link}",
title: "#{strip_markup(user_combined_name)} #{action} #{wiki_page_link}",
subtitle: "in #{project_link}",
text: title,
text: strip_markup(title),
image: user_avatar
}
end
......@@ -46,7 +46,7 @@ module Integrations
private
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
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
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
{
file: CarrierWaveStringFile.new(Base64.decode64(attachment['data'])),
size: attachment['length'],
size: calculated_package_file_size,
file_sha1: version_data[:dist][:shasum],
file_name: package_file_name,
build: params[:build]
......@@ -86,7 +100,7 @@ module Packages
end
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
......
......@@ -2,13 +2,17 @@
module Resolvers
class SecurityReportSummaryResolver < BaseResolver
type Types::SecurityReportSummaryType, null: true
include Gitlab::Graphql::Authorize::AuthorizeResource
type Types::SecurityReportSummaryType, null: true
authorize :read_security_resource
extras [:lookahead]
alias_method :pipeline, :object
def resolve(lookahead:)
return unless authorized_resource?(pipeline.project)
Security::ReportSummaryService.new(
pipeline,
selection_information(lookahead)
......
# frozen_string_literal: true
module Types
# rubocop: disable Graphql/AuthorizeTypes
class PipelineSecurityReportFindingType < BaseObject
graphql_name 'PipelineSecurityReportFinding'
description 'Represents vulnerability finding of a security report on the pipeline.'
authorize :read_security_resource
field :report_type,
type: VulnerabilityReportTypeEnum,
null: true,
......@@ -97,5 +98,4 @@ module Types
object.project.licensed_feature_available?(:sast_fp_reduction)
end
end
# rubocop: enable Graphql/AuthorizeTypes
end
......@@ -110,6 +110,8 @@ module Vulnerabilities
.where("vulnerability_occurrences.location -> 'kubernetes_resource' -> 'agent_id' ?| array[:agent_ids]", agent_ids: agent_ids)
end
alias_method :declarative_policy_subject, :project
def self.counted_by_severity
group(:severity).count.transform_keys do |severity|
severities[severity]
......
......@@ -328,6 +328,7 @@ module EE
rule { ip_enforcement_prevents_access & ~owner & ~auditor }.policy do
prevent :read_group
prevent :read_milestone
end
rule { owner & group_saml_enabled }.policy do
......
......@@ -342,6 +342,10 @@ module EE
rule { ip_enforcement_prevents_access & ~admin & ~auditor }.policy do
prevent :read_project
prevent :read_issue
prevent :read_merge_request
prevent :read_milestone
prevent :read_container_image
end
rule { locked_approvers_rules }.policy do
......
......@@ -5,9 +5,14 @@ require 'spec_helper'
RSpec.describe Resolvers::SecurityReportSummaryResolver do
include GraphqlHelpers
let_it_be(:pipeline) { 'pipeline' }
let_it_be(:pipeline) { create(:ci_pipeline) }
let_it_be(:user) { pipeline.project.owner }
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
let(:lookahead) do
build_mock_lookahead(expected_selection_info)
......@@ -32,7 +37,18 @@ RSpec.describe Resolvers::SecurityReportSummaryResolver do
) do |summary_service|
expect(summary_service).to receive(:execute).and_return({})
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
......@@ -61,10 +77,15 @@ RSpec.describe Resolvers::SecurityReportSummaryResolver do
) do |summary_service|
expect(summary_service).to receive(:execute).and_return({})
end
resolve(described_class, obj: pipeline, lookahead: lookahead)
resolve_security_report_summary
end
end
end
def resolve_security_report_summary
resolve(described_class, obj: pipeline, lookahead: lookahead, ctx: { current_user: user })
end
end
def build_mock_lookahead(structure)
......
......@@ -33,6 +33,7 @@ RSpec.describe GitlabSchema.types['PipelineSecurityReportFinding'] do
subject { GitlabSchema.execute(query, context: { current_user: user }).as_json }
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) }
......
......@@ -575,6 +575,7 @@ RSpec.describe GroupPolicy do
context 'without restriction' do
it { is_expected.to be_allowed(:read_group) }
it { is_expected.to be_allowed(:read_milestone) }
end
context 'with restriction' do
......@@ -586,6 +587,7 @@ RSpec.describe GroupPolicy do
let(:range) { '192.168.0.0/24' }
it { is_expected.to be_allowed(:read_group) }
it { is_expected.to be_allowed(:read_milestone) }
end
context 'address is outside the range' do
......@@ -593,18 +595,21 @@ RSpec.describe GroupPolicy do
context 'as developer' do
it { is_expected.to be_disallowed(:read_group) }
it { is_expected.to be_disallowed(:read_milestone) }
end
context 'as owner' do
let(:current_user) { owner }
it { is_expected.to be_allowed(:read_group) }
it { is_expected.to be_allowed(:read_milestone) }
end
context 'as auditor' do
let(:current_user) { create(:user, :auditor) }
it { is_expected.to be_allowed(:read_group) }
it { is_expected.to be_allowed(:read_milestone) }
end
end
end
......
......@@ -455,6 +455,10 @@ RSpec.describe ProjectPolicy do
context 'group without restriction' do
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
context 'group with restriction' do
......@@ -466,25 +470,45 @@ RSpec.describe ProjectPolicy do
let(:range) { '192.168.0.0/24' }
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
context 'address is outside the range' do
let(:range) { '10.0.0.0/8' }
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
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
context 'with admin disabled' do
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
context 'with auditor' do
let(:current_user) { create(:user, :auditor) }
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
......
......@@ -3,7 +3,9 @@
require 'spec_helper'
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(:user) { create(:user) }
......@@ -49,13 +51,16 @@ RSpec.describe 'Query.project(fullPath).pipeline(iid).securityReportFindings' do
)
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
stub_licensed_features(sast: true, dast: true)
stub_licensed_features(sast: true, dast: true, security_dashboard: true)
end
context 'when user is member of the project' 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
it 'returns no vulnerability findings' do
expect(security_report_findings).to be_nil
expect(security_report_findings).to be_blank
end
end
end
context 'when `sast` and `dast` both features are disabled' do
context 'when the required features are disabled' do
before do
stub_licensed_features(sast: false, dast: false)
stub_licensed_features(sast: false, dast: false, security_dashboard: false)
end
it 'returns no vulnerability findings' do
expect(security_report_findings).to be_nil
expect(security_report_findings).to be_blank
end
end
end
......@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'Query.project(fullPath).pipeline(iid).securityReportSummary' do
include GraphqlHelpers
let_it_be(:project) { create(:project, :repository) }
let_it_be(:pipeline) { create(:ci_pipeline, :success, project: project) }
......@@ -65,28 +67,54 @@ RSpec.describe 'Query.project(fullPath).pipeline(iid).securityReportSummary' do
)
end
before do
stub_licensed_features(sast: true, dependency_scanning: true, container_scanning: true, dast: true)
project.add_developer(user)
let(:security_report_summary) { subject.dig('project', 'pipeline', 'securityReportSummary') }
subject do
post_graphql(query, current_user: user)
graphql_data
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(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
expect(dast_scanned_resources.length).to eq(20)
end
it 'shows the first 20 scanned resources' do
dast_scanned_resources = security_report_summary.dig('dast', 'scannedResources', 'nodes')
it 'returns nil for the scannedResourcesCsvPath' do
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
it 'returns nil for the scannedResourcesCsvPath' do
expect(security_report_summary.dig('dast', 'scannedResourcesCsvPath')).to be_nil
context 'when the required features are disabled' do
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
......@@ -614,6 +614,7 @@ module API
source_project = Project.find_by_id(params[:project_id])
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
......
......@@ -5,16 +5,16 @@ module BulkImports
module Pipelines
class UploadsPipeline
include Pipeline
include Gitlab::ImportExport::CommandLineUtil
FILENAME = 'uploads.tar.gz'
AVATAR_PATTERN = %r{.*\/#{BulkImports::UploadsExportService::AVATAR_PATH}\/(?<identifier>.*)}.freeze
AvatarLoadingError = Class.new(StandardError)
def extract(context)
download_service(tmp_dir, context).execute
untar_zxf(archive: File.join(tmp_dir, FILENAME), dir: tmp_dir)
def extract(_context)
download_service.execute
decompression_service.execute
extraction_service.execute
upload_file_paths = Dir.glob(File.join(tmp_dir, '**', '*'))
BulkImports::Pipeline::ExtractedData.new(data: upload_file_paths)
......@@ -29,6 +29,7 @@ module BulkImports
return unless dynamic_path
return if File.directory?(file_path)
return if File.lstat(file_path).symlink?
named_captures = dynamic_path.named_captures.symbolize_keys
......@@ -36,20 +37,40 @@ module BulkImports
end
def after_run(_)
FileUtils.remove_entry(tmp_dir)
FileUtils.remove_entry(tmp_dir) if Dir.exist?(tmp_dir)
end
private
def download_service(tmp_dir, context)
def download_service
BulkImports::FileDownloadService.new(
configuration: context.configuration,
relative_url: context.entity.relation_download_url_path('uploads'),
relative_url: context.entity.relation_download_url_path(relation),
dir: tmp_dir,
filename: FILENAME
filename: targz_filename
)
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
@tmp_dir ||= Dir.mktmpdir('bulk_imports')
end
......
......@@ -18,6 +18,10 @@ module Gitlab
tar_with_options(archive: archive, dir: dir, options: 'cf')
end
def untar_xf(archive:, dir:)
untar_with_options(archive: archive, dir: dir, options: 'xf')
end
def gzip(dir:, filename:)
gzip_with_options(dir: dir, filename: filename)
end
......
......@@ -48,10 +48,11 @@ module Gitlab
)
end
def authorize_url(redirect_uri)
def authorize_url(redirect_uri, state = nil)
client.auth_code.authorize_url({
redirect_uri: redirect_uri,
scope: "repo, user, user:email"
scope: "repo, user, user:email",
state: state
})
end
......
......@@ -252,13 +252,13 @@ module Gitlab
def internal_web?(uri)
uri.scheme == config.gitlab.protocol &&
uri.hostname == config.gitlab.host &&
(uri.port.blank? || uri.port == config.gitlab.port)
get_port(uri) == config.gitlab.port
end
def internal_shell?(uri)
uri.scheme == 'ssh' &&
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
def domain_allowed?(uri)
......
......@@ -8,10 +8,6 @@ RSpec.describe Dashboard::ProjectsController, :aggregate_failures do
let_it_be(:user) { create(:user) }
describe '#index' do
context 'user not logged in' do
it_behaves_like 'authenticates sessionless user', :index, :atom
end
context 'user logged in' do
let_it_be(:project) { create(:project, name: 'Project 1') }
let_it_be(:project2) { create(:project, name: 'Project Two') }
......
......@@ -72,9 +72,6 @@ RSpec.describe DashboardController do
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
let(:user) { create(:user) }
......
......@@ -1209,26 +1209,6 @@ RSpec.describe GroupsController, factory_default: :keep do
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
before do
group.add_owner(user)
......
......@@ -6,6 +6,7 @@ RSpec.describe Import::GithubController do
include ImportSpecHelper
let(:provider) { :github }
let(:new_import_url) { public_send("new_import_#{provider}_url") }
include_context 'a GitHub-ish import controller'
......@@ -50,13 +51,37 @@ RSpec.describe Import::GithubController do
stub_omniauth_provider('github')
end
it "updates access token" do
token = "asdasd12345"
context "when auth state param is missing from session" do
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)
expect(controller).to redirect_to(status_import_github_url)
it "updates access token if state param is valid" do
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
......@@ -71,8 +96,6 @@ RSpec.describe Import::GithubController do
end
context 'when OAuth config is missing' do
let(:new_import_url) { public_send("new_import_#{provider}_url") }
before do
allow(controller).to receive(:oauth_config).and_return(nil)
end
......@@ -108,6 +131,16 @@ RSpec.describe Import::GithubController do
get :status
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
context 'when feature remove_legacy_github_client is enabled' do
......@@ -130,6 +163,16 @@ RSpec.describe Import::GithubController do
get :status
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 'when no page is specified' do
it 'requests first page' do
......
......@@ -162,27 +162,4 @@ RSpec.describe Projects::CommitsController do
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
......@@ -1948,40 +1948,4 @@ RSpec.describe Projects::IssuesController do
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
......@@ -128,6 +128,8 @@ RSpec.describe Projects::RawController do
let_it_be(:user) { create(:user, static_object_token: 'very-secure-token') }
let_it_be(:file_path) { 'master/README.md' }
let(:token) { user.static_object_token }
before do
project.add_developer(user)
end
......@@ -143,13 +145,36 @@ RSpec.describe Projects::RawController do
context 'when a token param is present' do
context 'when token is correct' do
let(:params) { { token: user.static_object_token } }
let(:params) { { token: token } }
it 'calls the action normally' do
get_show
expect(response).to have_gitlab_http_status(:ok)
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
context 'when token is incorrect' do
......@@ -165,18 +190,45 @@ RSpec.describe Projects::RawController do
end
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
it 'calls the action normally' do
request.headers['X-Gitlab-Static-Object-Token'] = user.static_object_token
get_show
expect(response).to have_gitlab_http_status(:ok)
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
context 'when token is incorrect' do
let(:token) { 'foobar' }
it 'redirects to sign in page' do
request.headers['X-Gitlab-Static-Object-Token'] = 'foobar'
get_show
expect(response).to have_gitlab_http_status(:found)
......
......@@ -178,6 +178,29 @@ RSpec.describe Projects::RepositoriesController do
expect(response).to have_gitlab_http_status(:ok)
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
context 'when token is incorrect' do
......@@ -197,6 +220,31 @@ RSpec.describe Projects::RepositoriesController do
expect(response).to have_gitlab_http_status(:ok)
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
context 'when token is incorrect' do
......
......@@ -117,28 +117,6 @@ RSpec.describe Projects::TagsController do
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
before do
project.add_developer(user)
......
......@@ -1568,28 +1568,6 @@ RSpec.describe ProjectsController do
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
let_it_be(:public_project) { create(:project, :public) }
let_it_be(:event) { create(:event, :commented, project: public_project, target: create(:note, project: public_project)) }
......
......@@ -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 () => {
const testPath = '/test-path.css';
jest.spyOn(EmojiUnicodeSupport, 'default').mockReturnValue(false);
......
......@@ -560,13 +560,13 @@ RSpec.describe Resolvers::IssuesResolver do
end
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)
end
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]
.map { |issue| resolve_issues(iid: issue.iid.to_s) }
.flat_map(&:to_a)
......@@ -576,7 +576,7 @@ RSpec.describe Resolvers::IssuesResolver do
end
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
end
......
......@@ -4,6 +4,12 @@ require 'spec_helper'
RSpec.describe 'ActionCableSubscriptionAdapterIdentifier override' 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
it 'does not override server config id with action cable pid' do
config = {
......
......@@ -70,8 +70,10 @@ RSpec.describe BulkImports::Common::Pipelines::UploadsPipeline do
describe '#extract' do
it 'downloads & extracts upload paths' do
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)
.to receive(:new)
......@@ -80,9 +82,13 @@ RSpec.describe BulkImports::Common::Pipelines::UploadsPipeline do
relative_url: "/#{entity.pluralized_name}/test/export_relations/download?relation=uploads",
dir: tmpdir,
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)
......@@ -106,6 +112,16 @@ RSpec.describe BulkImports::Common::Pipelines::UploadsPipeline do
expect { pipeline.load(context, uploads_dir_path) }.not_to change { portable.uploads.count }
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
......
......@@ -101,4 +101,39 @@ RSpec.describe Gitlab::ImportExport::CommandLineUtil do
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
......@@ -531,24 +531,6 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
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
context 'when enforce_user is' do
......@@ -611,6 +593,44 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do
expect(described_class).to be_blocked_url('http://foobar.x')
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
describe '#validate_hostname' do
......
......@@ -16,6 +16,8 @@ RSpec.describe Integrations::ChatMessage::AlertMessage do
}.merge(Gitlab::DataBuilder::Alert.build(alert))
end
it_behaves_like Integrations::ChatMessage
describe '#message' do
it 'returns the correct message' do
expect(subject.message).to eq("Alert firing in #{args[:project_name]}")
......
......@@ -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') }
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
......@@ -3,83 +3,79 @@
require 'spec_helper'
RSpec.describe Integrations::ChatMessage::DeploymentMessage do
describe '#pretext' do
it 'returns a message with the data returned by the deployment data builder' do
environment = create(:environment, name: "myenvironment")
project = create(:project, :repository)
commit = project.commit('HEAD')
deployment = create(:deployment, status: :success, environment: environment, project: project, sha: commit.sha)
data = Gitlab::DataBuilder::Deployment.build(deployment, Time.current)
subject { described_class.new(args) }
let_it_be(:user) { create(:user, name: 'John Smith', username: 'smith') }
let_it_be(:namespace) { create(:namespace, name: 'myspace') }
let_it_be(:project) { create(:project, :repository, namespace: namespace, name: 'myproject') }
let_it_be(:commit) { project.commit('HEAD') }
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
it 'returns a message for a successful deployment' do
data = {
args.merge!(
status: 'success',
environment: 'production'
}
)
message = described_class.new(data)
expect(message.pretext).to eq('Deploy to production succeeded')
expect(subject.pretext).to eq('Deploy to production succeeded')
end
it 'returns a message for a failed deployment' do
data = {
args.merge!(
status: 'failed',
environment: 'production'
}
)
message = described_class.new(data)
expect(message.pretext).to eq('Deploy to production failed')
expect(subject.pretext).to eq('Deploy to production failed')
end
it 'returns a message for a canceled deployment' do
data = {
args.merge!(
status: 'canceled',
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
it 'returns a message for a deployment to another environment' do
data = {
args.merge!(
status: 'success',
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
it 'returns a message for a deployment with any other status' do
data = {
args.merge!(
status: 'unknown',
environment: 'staging'
}
)
message = described_class.new(data)
expect(message.pretext).to eq('Deploy to staging unknown')
expect(subject.pretext).to eq('Deploy to staging unknown')
end
it 'returns a message for a running deployment' do
data = {
status: 'running',
environment: 'production'
}
message = described_class.new(data)
args.merge!(
status: 'running',
environment: 'production'
)
expect(message.pretext).to eq('Starting deploy to production')
expect(subject.pretext).to eq('Starting deploy to production')
end
end
......@@ -108,21 +104,11 @@ RSpec.describe Integrations::ChatMessage::DeploymentMessage do
end
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)
commit_url = Gitlab::UrlBuilder.build(deployment.commit)
user_url = Gitlab::Routing.url_helpers.user_url(user)
data = Gitlab::DataBuilder::Deployment.build(deployment, Time.current)
message = described_class.new(data)
expect(message.attachments).to eq([{
expect(subject.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}",
color: "good"
}])
......
......@@ -28,6 +28,8 @@ RSpec.describe Integrations::ChatMessage::IssueMessage do
}
end
it_behaves_like Integrations::ChatMessage
context 'without markdown' do
let(:color) { '#C95823' }
......
......@@ -29,6 +29,8 @@ RSpec.describe Integrations::ChatMessage::MergeMessage do
}
end
it_behaves_like Integrations::ChatMessage
context 'without markdown' do
let(:color) { '#345' }
......
......@@ -19,6 +19,10 @@ RSpec.describe Integrations::ChatMessage::NoteMessage do
name: 'project_name',
url: 'http://somewhere.com'
},
commit: {
id: '5f163b2b95e6f53cbd428f5f0b103702a52b9a23',
message: "Added a commit message\ndetails\n123\n"
},
object_attributes: {
id: 10,
note: 'comment on a commit',
......@@ -28,16 +32,9 @@ RSpec.describe Integrations::ChatMessage::NoteMessage do
}
end
context 'commit notes' do
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
it_behaves_like Integrations::ChatMessage
context 'commit notes' do
context 'without markdown' do
it 'returns a message regarding notes on commits' do
expect(subject.pretext).to eq("Test User (test.user) <http://url.com|commented on " \
......
......@@ -40,6 +40,8 @@ RSpec.describe Integrations::ChatMessage::PipelineMessage do
let(:has_yaml_errors) { false }
it_behaves_like Integrations::ChatMessage
before do
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])
......
......@@ -19,6 +19,8 @@ RSpec.describe Integrations::ChatMessage::PushMessage do
let(:color) { '#345' }
it_behaves_like Integrations::ChatMessage
context 'push' do
before do
args[:commits] = [
......
......@@ -33,6 +33,8 @@ RSpec.describe Integrations::ChatMessage::WikiPageMessage do
}
end
it_behaves_like Integrations::ChatMessage
context 'without markdown' do
describe '#pretext' do
context 'when :action == "create"' do
......
......@@ -253,7 +253,7 @@ RSpec.describe 'GraphQL' do
end
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
stub_authentication_activity_metrics(debug: false)
......@@ -276,6 +276,32 @@ RSpec.describe 'GraphQL' do
expect(graphql_errors).to include({ 'message' => /API not accessible/ })
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
it 'does not log the user in' do
token.update!(scopes: [:read_user])
......
......@@ -3138,6 +3138,29 @@ RSpec.describe API::Projects do
expect(json_response['message']).to eq('404 Project Not Found')
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
allow_next_instance_of(::ProjectTeam) do |project_team|
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
let_it_be(:project) { issue.project }
let_it_be(:user) { issue.author }
before do
login_as(user)
end
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_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) }
......@@ -68,4 +68,38 @@ RSpec.describe Projects::IssuesController do
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
# 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
end
context 'token authentication' do
let(:url) { user_url(user.username, format: :atom) }
it_behaves_like 'authenticates sessionless user for the request spec', public: true
it_behaves_like 'authenticates sessionless user for the request spec', 'show atom', public_resource: true do
let(:url) { user_url(user, format: :atom) }
end
end
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
Gitlab::Json.parse(fixture_file('packages/npm/payload.json')
.gsub('@root/npm-test', package_name)
.gsub('1.0.1', version)).with_indifferent_access
.merge!(override)
end
let(:override) { {} }
let(:package_name) { "@#{namespace.path}/my-app" }
let(:version_data) { params.dig('versions', '1.0.1') }
......@@ -116,13 +114,53 @@ RSpec.describe Packages::Npm::CreatePackageService do
it { expect(subject[:message]).to be 'Package already exists.' }
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
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
it { expect(subject[:http_status]).to eq 400 }
it { expect(subject[:message]).to be 'File is too large.' }
context 'when max file size is exceeded' do
# 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
[
......@@ -141,7 +179,7 @@ RSpec.describe Packages::Npm::CreatePackageService do
end
context 'with empty versions' do
let(:override) { { versions: {} } }
let(:params) { super().merge!({ versions: {} } ) }
it { expect(subject[:http_status]).to eq 400 }
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
RSpec.shared_examples 'authenticates sessionless user for the request spec' do |params|
params ||= {}
RSpec.shared_examples 'authenticates sessionless user for the request spec' do |name, public_resource:, ignore_metrics: false, params: {}|
before do
stub_authentication_activity_metrics(debug: false)
end
let(:user) { create(:user) }
let_it_be(:user) { create(: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
it 'logs the user in' do
shared_examples 'authenticates user and returns response with ok status' do
it 'authenticates user and returns response with ok status' do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
.and increment(:user_session_override_counter)
.and increment(:user_sessionless_authentication_counter)
.and increment(:user_session_override_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(response).to have_gitlab_http_status(:ok)
end
end
it 'does not log the user in if page is public', if: params[:public] do
get url, params: default_params
shared_examples 'does not authenticate user and returns response with ok status' do
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(response).to have_gitlab_http_status(:ok)
end
end
context 'when the personal access token has no api scope', unless: params[:public] do
it 'does not log the user in' do
shared_examples 'does not return response with ok status' do
it 'does not return response with ok status' 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]
unless ignore_metrics
expect(authentication_metrics)
.to increment(:user_unauthenticated_counter)
end
personal_access_token.update!(scopes: [:read_user])
get url, params: default_params.merge(private_token: personal_access_token.token)
subject
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)
shared_examples 'using valid token' do
context 'when resource is private', unless: public_resource do
include_examples 'authenticates user and returns response with ok status'
headers = { 'PRIVATE-TOKEN': personal_access_token.token }
get url, params: default_params, headers: headers
context 'when user with expired password' do
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
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)
shared_examples 'using invalid token' do
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
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
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
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