Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
db3acec198
commit
9248363e3e
33 changed files with 291 additions and 130 deletions
2
Gemfile
2
Gemfile
|
@ -145,7 +145,7 @@ gem 'aws-sdk-s3', '~> 1'
|
|||
gem 'faraday_middleware-aws-sigv4', '~>0.3.0'
|
||||
|
||||
# Markdown and HTML processing
|
||||
gem 'html-pipeline', '~> 2.12'
|
||||
gem 'html-pipeline', '~> 2.13.2'
|
||||
gem 'deckar01-task_list', '2.3.1'
|
||||
gem 'gitlab-markup', '~> 1.7.1'
|
||||
gem 'github-markup', '~> 1.7.0', require: 'github/markup'
|
||||
|
|
|
@ -577,7 +577,7 @@ GEM
|
|||
hipchat (1.5.2)
|
||||
httparty
|
||||
mimemagic
|
||||
html-pipeline (2.12.2)
|
||||
html-pipeline (2.13.2)
|
||||
activesupport (>= 2)
|
||||
nokogiri (>= 1.4)
|
||||
html2text (0.2.0)
|
||||
|
@ -1391,7 +1391,7 @@ DEPENDENCIES
|
|||
hashie-forbidden_attributes
|
||||
health_check (~> 3.0)
|
||||
hipchat (~> 1.5.0)
|
||||
html-pipeline (~> 2.12)
|
||||
html-pipeline (~> 2.13.2)
|
||||
html2text
|
||||
httparty (~> 0.16.4)
|
||||
icalendar
|
||||
|
|
|
@ -17,7 +17,6 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController
|
|||
@scope = params[:scope]
|
||||
@all_schedules = Ci::PipelineSchedulesFinder.new(@project).execute
|
||||
@schedules = Ci::PipelineSchedulesFinder.new(@project).execute(scope: params[:scope])
|
||||
.includes(:last_pipeline)
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Index ci_pipelines on pipeline_schedule_id and id
|
||||
merge_request: 50478
|
||||
author:
|
||||
type: performance
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix LDAP override throws 404 when member has Minimal access
|
||||
merge_request: 50680
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix empty pipeline analytics charts when time_zone is non-UTC
|
||||
merge_request: 50760
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ReindexCiPipelinesOnScheduleIdAndId < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
OLD_INDEX_NAME = 'index_ci_pipelines_on_pipeline_schedule_id'
|
||||
NEW_INDEX_NAME = 'index_ci_pipelines_on_pipeline_schedule_id_and_id'
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_index :ci_pipelines, [:pipeline_schedule_id, :id], name: NEW_INDEX_NAME
|
||||
remove_concurrent_index_by_name :ci_pipelines, OLD_INDEX_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
add_concurrent_index :ci_pipelines, :pipeline_schedule_id, name: OLD_INDEX_NAME
|
||||
remove_concurrent_index_by_name :ci_pipelines, NEW_INDEX_NAME
|
||||
end
|
||||
end
|
1
db/schema_migrations/20201223012231
Normal file
1
db/schema_migrations/20201223012231
Normal file
|
@ -0,0 +1 @@
|
|||
e845a6704ac92881926cca56bf7fb01c6252f1fe2b2d94fc9d6548144126d6a5
|
|
@ -21088,7 +21088,7 @@ CREATE INDEX index_ci_pipelines_on_external_pull_request_id ON ci_pipelines USIN
|
|||
|
||||
CREATE INDEX index_ci_pipelines_on_merge_request_id ON ci_pipelines USING btree (merge_request_id) WHERE (merge_request_id IS NOT NULL);
|
||||
|
||||
CREATE INDEX index_ci_pipelines_on_pipeline_schedule_id ON ci_pipelines USING btree (pipeline_schedule_id);
|
||||
CREATE INDEX index_ci_pipelines_on_pipeline_schedule_id_and_id ON ci_pipelines USING btree (pipeline_schedule_id, id);
|
||||
|
||||
CREATE INDEX index_ci_pipelines_on_project_id_and_id_desc ON ci_pipelines USING btree (project_id, id DESC);
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ module Banzai
|
|||
TABLE_GRID_CLASSES = %w(grid-all grid-rows grid-cols grid-none).freeze
|
||||
TABLE_STRIPES_CLASSES = %w(stripes-all stripes-odd stripes-even stripes-hover stripes-none).freeze
|
||||
|
||||
ELEMENT_CLASSES_WHITELIST = {
|
||||
ELEMENT_CLASSES_ALLOWLIST = {
|
||||
span: %w(big small underline overline line-through).freeze,
|
||||
div: ALIGNMENT_BUILTINS_CLASSES + ['admonitionblock'].freeze,
|
||||
td: ['icon'].freeze,
|
||||
|
@ -38,35 +38,35 @@ module Banzai
|
|||
table: TABLE_FRAME_CLASSES + TABLE_GRID_CLASSES + TABLE_STRIPES_CLASSES
|
||||
}.freeze
|
||||
|
||||
def customize_whitelist(whitelist)
|
||||
def customize_allowlist(allowlist)
|
||||
# Allow marks
|
||||
whitelist[:elements].push('mark')
|
||||
allowlist[:elements].push('mark')
|
||||
|
||||
# Allow any classes in `span`, `i`, `div`, `td`, `ul`, `ol` and `a` elements
|
||||
# but then remove any unknown classes
|
||||
whitelist[:attributes]['span'] = %w(class)
|
||||
whitelist[:attributes]['div'].push('class')
|
||||
whitelist[:attributes]['td'] = %w(class)
|
||||
whitelist[:attributes]['i'] = %w(class)
|
||||
whitelist[:attributes]['ul'] = %w(class)
|
||||
whitelist[:attributes]['ol'] = %w(class)
|
||||
whitelist[:attributes]['a'].push('class')
|
||||
whitelist[:attributes]['table'] = %w(class)
|
||||
whitelist[:transformers].push(self.class.remove_element_classes)
|
||||
allowlist[:attributes]['span'] = %w(class)
|
||||
allowlist[:attributes]['div'].push('class')
|
||||
allowlist[:attributes]['td'] = %w(class)
|
||||
allowlist[:attributes]['i'] = %w(class)
|
||||
allowlist[:attributes]['ul'] = %w(class)
|
||||
allowlist[:attributes]['ol'] = %w(class)
|
||||
allowlist[:attributes]['a'].push('class')
|
||||
allowlist[:attributes]['table'] = %w(class)
|
||||
allowlist[:transformers].push(self.class.remove_element_classes)
|
||||
|
||||
# Allow `id` in anchor and footnote elements
|
||||
whitelist[:attributes]['a'].push('id')
|
||||
whitelist[:attributes]['div'].push('id')
|
||||
allowlist[:attributes]['a'].push('id')
|
||||
allowlist[:attributes]['div'].push('id')
|
||||
|
||||
# Allow `id` in heading elements for section anchors
|
||||
SECTION_HEADINGS.each do |header|
|
||||
whitelist[:attributes][header] = %w(id)
|
||||
allowlist[:attributes][header] = %w(id)
|
||||
end
|
||||
|
||||
# Remove ids that are not explicitly allowed
|
||||
whitelist[:transformers].push(self.class.remove_disallowed_ids)
|
||||
allowlist[:transformers].push(self.class.remove_disallowed_ids)
|
||||
|
||||
whitelist
|
||||
allowlist
|
||||
end
|
||||
|
||||
class << self
|
||||
|
@ -91,11 +91,11 @@ module Banzai
|
|||
lambda do |env|
|
||||
node = env[:node]
|
||||
|
||||
return unless (classes_whitelist = ELEMENT_CLASSES_WHITELIST[node.name.to_sym])
|
||||
return unless (classes_allowlist = ELEMENT_CLASSES_ALLOWLIST[node.name.to_sym])
|
||||
return unless node.has_attribute?('class')
|
||||
|
||||
classes = node['class'].strip.split(' ')
|
||||
allowed_classes = (classes & classes_whitelist)
|
||||
allowed_classes = (classes & classes_allowlist)
|
||||
if allowed_classes.empty?
|
||||
node.remove_attribute('class')
|
||||
else
|
||||
|
|
|
@ -15,7 +15,7 @@ module Banzai
|
|||
needs(:asset_proxy, :asset_proxy_secret_key) if asset_proxy_enabled?
|
||||
end
|
||||
|
||||
def asset_host_whitelisted?(host)
|
||||
def asset_host_allowed?(host)
|
||||
context[:asset_proxy_domain_regexp] ? context[:asset_proxy_domain_regexp].match?(host) : false
|
||||
end
|
||||
|
||||
|
@ -44,21 +44,21 @@ module Banzai
|
|||
Gitlab.config.asset_proxy['enabled'] = application_settings.asset_proxy_enabled
|
||||
Gitlab.config.asset_proxy['url'] = application_settings.asset_proxy_url
|
||||
Gitlab.config.asset_proxy['secret_key'] = application_settings.asset_proxy_secret_key
|
||||
Gitlab.config.asset_proxy['whitelist'] = determine_whitelist(application_settings)
|
||||
Gitlab.config.asset_proxy['domain_regexp'] = compile_whitelist(Gitlab.config.asset_proxy.whitelist)
|
||||
Gitlab.config.asset_proxy['allowlist'] = determine_allowlist(application_settings)
|
||||
Gitlab.config.asset_proxy['domain_regexp'] = compile_allowlist(Gitlab.config.asset_proxy.allowlist)
|
||||
else
|
||||
Gitlab.config.asset_proxy['enabled'] = ::ApplicationSetting.defaults[:asset_proxy_enabled]
|
||||
end
|
||||
end
|
||||
|
||||
def self.compile_whitelist(domain_list)
|
||||
def self.compile_allowlist(domain_list)
|
||||
return if domain_list.empty?
|
||||
|
||||
escaped = domain_list.map { |domain| Regexp.escape(domain).gsub('\*', '.*?') }
|
||||
Regexp.new("^(#{escaped.join('|')})$", Regexp::IGNORECASE)
|
||||
end
|
||||
|
||||
def self.determine_whitelist(application_settings)
|
||||
def self.determine_allowlist(application_settings)
|
||||
application_settings.asset_proxy_whitelist.presence || [Gitlab.config.gitlab.host]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -16,42 +16,42 @@ module Banzai
|
|||
|
||||
UNSAFE_PROTOCOLS = %w(data javascript vbscript).freeze
|
||||
|
||||
def whitelist
|
||||
strong_memoize(:whitelist) do
|
||||
whitelist = super.deep_dup
|
||||
def allowlist
|
||||
strong_memoize(:allowlist) do
|
||||
allowlist = super.deep_dup
|
||||
|
||||
# Allow span elements
|
||||
whitelist[:elements].push('span')
|
||||
allowlist[:elements].push('span')
|
||||
|
||||
# Allow data-math-style attribute in order to support LaTeX formatting
|
||||
whitelist[:attributes]['code'] = %w(data-math-style)
|
||||
whitelist[:attributes]['pre'] = %w(data-math-style data-mermaid-style data-kroki-style)
|
||||
allowlist[:attributes]['code'] = %w(data-math-style)
|
||||
allowlist[:attributes]['pre'] = %w(data-math-style data-mermaid-style data-kroki-style)
|
||||
|
||||
# Allow html5 details/summary elements
|
||||
whitelist[:elements].push('details')
|
||||
whitelist[:elements].push('summary')
|
||||
allowlist[:elements].push('details')
|
||||
allowlist[:elements].push('summary')
|
||||
|
||||
# Allow abbr elements with title attribute
|
||||
whitelist[:elements].push('abbr')
|
||||
whitelist[:attributes]['abbr'] = %w(title)
|
||||
allowlist[:elements].push('abbr')
|
||||
allowlist[:attributes]['abbr'] = %w(title)
|
||||
|
||||
# Disallow `name` attribute globally, allow on `a`
|
||||
whitelist[:attributes][:all].delete('name')
|
||||
whitelist[:attributes]['a'].push('name')
|
||||
allowlist[:attributes][:all].delete('name')
|
||||
allowlist[:attributes]['a'].push('name')
|
||||
|
||||
# Allow any protocol in `a` elements
|
||||
# and then remove links with unsafe protocols
|
||||
whitelist[:protocols].delete('a')
|
||||
whitelist[:transformers].push(self.class.method(:remove_unsafe_links))
|
||||
allowlist[:protocols].delete('a')
|
||||
allowlist[:transformers].push(self.class.method(:remove_unsafe_links))
|
||||
|
||||
# Remove `rel` attribute from `a` elements
|
||||
whitelist[:transformers].push(self.class.remove_rel)
|
||||
allowlist[:transformers].push(self.class.remove_rel)
|
||||
|
||||
customize_whitelist(whitelist)
|
||||
customize_allowlist(allowlist)
|
||||
end
|
||||
end
|
||||
|
||||
def customize_whitelist(whitelist)
|
||||
def customize_allowlist(allowlist)
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
|
|
|
@ -6,14 +6,14 @@ module Banzai
|
|||
#
|
||||
# Extends Banzai::Filter::BaseSanitizationFilter with specific rules.
|
||||
class BroadcastMessageSanitizationFilter < Banzai::Filter::BaseSanitizationFilter
|
||||
def customize_whitelist(whitelist)
|
||||
whitelist[:elements].push('br')
|
||||
def customize_allowlist(allowlist)
|
||||
allowlist[:elements].push('br')
|
||||
|
||||
whitelist[:attributes]['a'].push('class', 'style')
|
||||
allowlist[:attributes]['a'].push('class', 'style')
|
||||
|
||||
whitelist[:css] = { properties: %w(color border background padding margin text-decoration) }
|
||||
allowlist[:css] = { properties: %w(color border background padding margin text-decoration) }
|
||||
|
||||
whitelist
|
||||
allowlist
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,26 +9,26 @@ module Banzai
|
|||
# Styles used by Markdown for table alignment
|
||||
TABLE_ALIGNMENT_PATTERN = /text-align: (?<alignment>center|left|right)/.freeze
|
||||
|
||||
def customize_whitelist(whitelist)
|
||||
# Allow table alignment; we whitelist specific text-align values in a
|
||||
def customize_allowlist(allowlist)
|
||||
# Allow table alignment; we allow specific text-align values in a
|
||||
# transformer below
|
||||
whitelist[:attributes]['th'] = %w(style)
|
||||
whitelist[:attributes]['td'] = %w(style)
|
||||
whitelist[:css] = { properties: ['text-align'] }
|
||||
allowlist[:attributes]['th'] = %w(style)
|
||||
allowlist[:attributes]['td'] = %w(style)
|
||||
allowlist[:css] = { properties: ['text-align'] }
|
||||
|
||||
# Allow the 'data-sourcepos' from CommonMark on all elements
|
||||
whitelist[:attributes][:all].push('data-sourcepos')
|
||||
allowlist[:attributes][:all].push('data-sourcepos')
|
||||
|
||||
# Remove any `style` properties not required for table alignment
|
||||
whitelist[:transformers].push(self.class.remove_unsafe_table_style)
|
||||
allowlist[:transformers].push(self.class.remove_unsafe_table_style)
|
||||
|
||||
# Allow `id` in a and li elements for footnotes
|
||||
# and remove any `id` properties not matching for footnotes
|
||||
whitelist[:attributes]['a'].push('id')
|
||||
whitelist[:attributes]['li'] = %w(id)
|
||||
whitelist[:transformers].push(self.class.remove_non_footnote_ids)
|
||||
allowlist[:attributes]['a'].push('id')
|
||||
allowlist[:attributes]['li'] = %w(id)
|
||||
allowlist[:transformers].push(self.class.remove_non_footnote_ids)
|
||||
|
||||
whitelist
|
||||
allowlist
|
||||
end
|
||||
|
||||
class << self
|
||||
|
|
|
@ -3,14 +3,14 @@
|
|||
module Banzai
|
||||
module Pipeline
|
||||
class DescriptionPipeline < FullPipeline
|
||||
WHITELIST = Banzai::Filter::SanitizationFilter::LIMITED.deep_dup.merge(
|
||||
ALLOWLIST = Banzai::Filter::SanitizationFilter::LIMITED.deep_dup.merge(
|
||||
elements: Banzai::Filter::SanitizationFilter::LIMITED[:elements] - %w(pre code img ol ul li)
|
||||
)
|
||||
|
||||
def self.transform_context(context)
|
||||
super(context).merge(
|
||||
# SanitizationFilter
|
||||
whitelist: WHITELIST
|
||||
allowlist: ALLOWLIST
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -92,10 +92,10 @@ module Gitlab
|
|||
# We only allow Private Access Tokens with `api` scope to be used by web
|
||||
# requests on RSS feeds or ICS files for backwards compatibility.
|
||||
# It is also used by GraphQL/API requests.
|
||||
def find_user_from_web_access_token(request_format)
|
||||
def find_user_from_web_access_token(request_format, scopes: [:api])
|
||||
return unless access_token && valid_web_access_format?(request_format)
|
||||
|
||||
validate_access_token!(scopes: [:api])
|
||||
validate_access_token!(scopes: scopes)
|
||||
|
||||
::PersonalAccessTokens::LastUsedService.new(access_token).execute
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def find_sessionless_user(request_format)
|
||||
find_user_from_web_access_token(request_format) ||
|
||||
find_user_from_web_access_token(request_format, scopes: [:api, :read_api]) ||
|
||||
find_user_from_feed_token(request_format) ||
|
||||
find_user_from_static_object_token(request_format) ||
|
||||
find_user_from_basic_auth_job ||
|
||||
|
|
|
@ -31,9 +31,10 @@ module Gitlab
|
|||
|
||||
current = @from
|
||||
while current <= @to
|
||||
@labels << current.strftime(@format)
|
||||
@total << (totals_count[current] || 0)
|
||||
@success << (success_count[current] || 0)
|
||||
label = current.strftime(@format)
|
||||
@labels << label
|
||||
@total << (totals_count[label] || 0)
|
||||
@success << (success_count[label] || 0)
|
||||
|
||||
current += interval_step
|
||||
end
|
||||
|
@ -45,6 +46,7 @@ module Gitlab
|
|||
query
|
||||
.group("date_trunc('#{interval}', #{::Ci::Pipeline.table_name}.created_at)")
|
||||
.count(:created_at)
|
||||
.transform_keys { |date| date.strftime(@format) }
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
|
|
|
@ -19559,12 +19559,21 @@ msgstr ""
|
|||
msgid "OnDemandScans|Create a new site profile"
|
||||
msgstr ""
|
||||
|
||||
msgid "OnDemandScans|Description"
|
||||
msgstr ""
|
||||
|
||||
msgid "OnDemandScans|Edit on-demand DAST scan"
|
||||
msgstr ""
|
||||
|
||||
msgid "OnDemandScans|For example: Tests the login page for SQL injections"
|
||||
msgstr ""
|
||||
|
||||
msgid "OnDemandScans|Manage profiles"
|
||||
msgstr ""
|
||||
|
||||
msgid "OnDemandScans|My daily scan"
|
||||
msgstr ""
|
||||
|
||||
msgid "OnDemandScans|New on-demand DAST scan"
|
||||
msgstr ""
|
||||
|
||||
|
@ -19583,6 +19592,15 @@ msgstr ""
|
|||
msgid "OnDemandScans|Run scan"
|
||||
msgstr ""
|
||||
|
||||
msgid "OnDemandScans|Save and run scan"
|
||||
msgstr ""
|
||||
|
||||
msgid "OnDemandScans|Save scan"
|
||||
msgstr ""
|
||||
|
||||
msgid "OnDemandScans|Scan name"
|
||||
msgstr ""
|
||||
|
||||
msgid "OnDemandScans|Scanner profile"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -146,7 +146,7 @@
|
|||
"visibilityjs": "^1.2.4",
|
||||
"vue": "^2.6.12",
|
||||
"vue-apollo": "^3.0.3",
|
||||
"vue-loader": "^15.9.5",
|
||||
"vue-loader": "^15.9.6",
|
||||
"vue-router": "3.4.9",
|
||||
"vue-template-compiler": "^2.6.12",
|
||||
"vue-virtual-scroll-list": "^1.4.4",
|
||||
|
|
|
@ -35,8 +35,8 @@ RSpec.describe Banzai::Filter::AssetProxyFilter do
|
|||
expect(Gitlab.config.asset_proxy.enabled).to be_truthy
|
||||
expect(Gitlab.config.asset_proxy.secret_key).to eq 'shared-secret'
|
||||
expect(Gitlab.config.asset_proxy.url).to eq 'https://assets.example.com'
|
||||
expect(Gitlab.config.asset_proxy.whitelist).to eq %w(gitlab.com *.mydomain.com)
|
||||
expect(Gitlab.config.asset_proxy.domain_regexp).to eq /^(gitlab\.com|.*?\.mydomain\.com)$/i
|
||||
expect(Gitlab.config.asset_proxy.allowlist).to eq %w(gitlab.com *.mydomain.com)
|
||||
expect(Gitlab.config.asset_proxy.domain_regexp).to eq(/^(gitlab\.com|.*?\.mydomain\.com)$/i)
|
||||
end
|
||||
|
||||
context 'when whitelist is empty' do
|
||||
|
@ -46,7 +46,7 @@ RSpec.describe Banzai::Filter::AssetProxyFilter do
|
|||
|
||||
described_class.initialize_settings
|
||||
|
||||
expect(Gitlab.config.asset_proxy.whitelist).to eq [Gitlab.config.gitlab.host]
|
||||
expect(Gitlab.config.asset_proxy.allowlist).to eq [Gitlab.config.gitlab.host]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -56,8 +56,8 @@ RSpec.describe Banzai::Filter::AssetProxyFilter do
|
|||
stub_asset_proxy_setting(enabled: true)
|
||||
stub_asset_proxy_setting(secret_key: 'shared-secret')
|
||||
stub_asset_proxy_setting(url: 'https://assets.example.com')
|
||||
stub_asset_proxy_setting(whitelist: %W(gitlab.com *.mydomain.com #{Gitlab.config.gitlab.host}))
|
||||
stub_asset_proxy_setting(domain_regexp: described_class.compile_whitelist(Gitlab.config.asset_proxy.whitelist))
|
||||
stub_asset_proxy_setting(allowlist: %W(gitlab.com *.mydomain.com #{Gitlab.config.gitlab.host}))
|
||||
stub_asset_proxy_setting(domain_regexp: described_class.compile_allowlist(Gitlab.config.asset_proxy.allowlist))
|
||||
@context = described_class.transform_context({})
|
||||
end
|
||||
|
||||
|
|
|
@ -5,9 +5,9 @@ require 'spec_helper'
|
|||
RSpec.describe Banzai::Filter::BroadcastMessageSanitizationFilter do
|
||||
include FilterSpecHelper
|
||||
|
||||
it_behaves_like 'default whitelist'
|
||||
it_behaves_like 'default allowlist'
|
||||
|
||||
describe 'custom whitelist' do
|
||||
describe 'custom allowlist' do
|
||||
it_behaves_like 'XSS prevention'
|
||||
it_behaves_like 'sanitize link'
|
||||
|
||||
|
@ -26,19 +26,19 @@ RSpec.describe Banzai::Filter::BroadcastMessageSanitizationFilter do
|
|||
end
|
||||
|
||||
context 'when `a` elements have `style` attribute' do
|
||||
let(:whitelisted_style) { 'color: red; border: blue; background: green; padding: 10px; margin: 10px; text-decoration: underline;' }
|
||||
let(:allowed_style) { 'color: red; border: blue; background: green; padding: 10px; margin: 10px; text-decoration: underline;' }
|
||||
|
||||
context 'allows specific properties' do
|
||||
let(:exp) { %{<a href="#" style="#{whitelisted_style}">Stylish Link</a>} }
|
||||
let(:exp) { %{<a href="#" style="#{allowed_style}">Stylish Link</a>} }
|
||||
|
||||
it { is_expected.to eq(exp) }
|
||||
end
|
||||
|
||||
it 'disallows other properties in `style` attribute on `a` elements' do
|
||||
style = [whitelisted_style, 'position: fixed'].join(';')
|
||||
style = [allowed_style, 'position: fixed'].join(';')
|
||||
doc = filter(%{<a href="#" style="#{style}">Stylish Link</a>})
|
||||
|
||||
expect(doc.at_css('a')['style']).to eq(whitelisted_style)
|
||||
expect(doc.at_css('a')['style']).to eq(allowed_style)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -5,31 +5,31 @@ require 'spec_helper'
|
|||
RSpec.describe Banzai::Filter::SanitizationFilter do
|
||||
include FilterSpecHelper
|
||||
|
||||
it_behaves_like 'default whitelist'
|
||||
it_behaves_like 'default allowlist'
|
||||
|
||||
describe 'custom whitelist' do
|
||||
describe 'custom allowlist' do
|
||||
it_behaves_like 'XSS prevention'
|
||||
it_behaves_like 'sanitize link'
|
||||
|
||||
it 'customizes the whitelist only once' do
|
||||
it 'customizes the allowlist only once' do
|
||||
instance = described_class.new('Foo')
|
||||
control_count = instance.whitelist[:transformers].size
|
||||
control_count = instance.allowlist[:transformers].size
|
||||
|
||||
3.times { instance.whitelist }
|
||||
3.times { instance.allowlist }
|
||||
|
||||
expect(instance.whitelist[:transformers].size).to eq control_count
|
||||
expect(instance.allowlist[:transformers].size).to eq control_count
|
||||
end
|
||||
|
||||
it 'customizes the whitelist only once for different instances' do
|
||||
it 'customizes the allowlist only once for different instances' do
|
||||
instance1 = described_class.new('Foo1')
|
||||
instance2 = described_class.new('Foo2')
|
||||
control_count = instance1.whitelist[:transformers].size
|
||||
control_count = instance1.allowlist[:transformers].size
|
||||
|
||||
instance1.whitelist
|
||||
instance2.whitelist
|
||||
instance1.allowlist
|
||||
instance2.allowlist
|
||||
|
||||
expect(instance1.whitelist[:transformers].size).to eq control_count
|
||||
expect(instance2.whitelist[:transformers].size).to eq control_count
|
||||
expect(instance1.allowlist[:transformers].size).to eq control_count
|
||||
expect(instance2.allowlist[:transformers].size).to eq control_count
|
||||
end
|
||||
|
||||
it 'sanitizes `class` attribute from all elements' do
|
||||
|
|
|
@ -21,7 +21,7 @@ RSpec.describe Banzai::Pipeline::DescriptionPipeline do
|
|||
stub_commonmark_sourcepos_disabled
|
||||
end
|
||||
|
||||
it 'uses a limited whitelist' do
|
||||
it 'uses a limited allowlist' do
|
||||
doc = parse('# Description')
|
||||
|
||||
expect(doc.strip).to eq 'Description'
|
||||
|
|
|
@ -176,8 +176,8 @@ RSpec.describe Banzai::Pipeline::GfmPipeline do
|
|||
stub_asset_proxy_setting(enabled: true)
|
||||
stub_asset_proxy_setting(secret_key: 'shared-secret')
|
||||
stub_asset_proxy_setting(url: 'https://assets.example.com')
|
||||
stub_asset_proxy_setting(whitelist: %W(gitlab.com *.mydomain.com #{Gitlab.config.gitlab.host}))
|
||||
stub_asset_proxy_setting(domain_regexp: Banzai::Filter::AssetProxyFilter.compile_whitelist(Gitlab.config.asset_proxy.whitelist))
|
||||
stub_asset_proxy_setting(allowlist: %W(gitlab.com *.mydomain.com #{Gitlab.config.gitlab.host}))
|
||||
stub_asset_proxy_setting(domain_regexp: Banzai::Filter::AssetProxyFilter.compile_allowlist(Gitlab.config.asset_proxy.allowlist))
|
||||
end
|
||||
|
||||
it 'replaces a lazy loaded img src' do
|
||||
|
|
|
@ -17,12 +17,12 @@ RSpec.describe Gitlab::AssetProxy do
|
|||
|
||||
context 'when asset proxy is enabled' do
|
||||
before do
|
||||
stub_asset_proxy_setting(whitelist: %w(gitlab.com *.mydomain.com))
|
||||
stub_asset_proxy_setting(allowlist: %w(gitlab.com *.mydomain.com))
|
||||
stub_asset_proxy_setting(
|
||||
enabled: true,
|
||||
url: 'https://assets.example.com',
|
||||
secret_key: 'shared-secret',
|
||||
domain_regexp: Banzai::Filter::AssetProxyFilter.compile_whitelist(Gitlab.config.asset_proxy.whitelist)
|
||||
domain_regexp: Banzai::Filter::AssetProxyFilter.compile_allowlist(Gitlab.config.asset_proxy.allowlist)
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
@ -6,7 +6,8 @@ RSpec.describe Gitlab::Auth::AuthFinders do
|
|||
include described_class
|
||||
include HttpBasicAuthHelpers
|
||||
|
||||
let(:user) { create(:user) }
|
||||
# Create the feed_token and static_object_token for the user
|
||||
let_it_be(:user) { create(:user).tap(&:feed_token).tap(&:static_object_token) }
|
||||
let(:env) do
|
||||
{
|
||||
'rack.input' => ''
|
||||
|
@ -65,7 +66,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do
|
|||
end
|
||||
|
||||
describe '#find_user_from_bearer_token' do
|
||||
let(:job) { create(:ci_build, user: user) }
|
||||
let_it_be_with_reload(:job) { create(:ci_build, user: user) }
|
||||
|
||||
subject { find_user_from_bearer_token }
|
||||
|
||||
|
@ -91,7 +92,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do
|
|||
end
|
||||
|
||||
context 'with a personal access token' do
|
||||
let(:pat) { create(:personal_access_token, user: user) }
|
||||
let_it_be(:pat) { create(:personal_access_token, user: user) }
|
||||
let(:token) { pat.token }
|
||||
|
||||
before do
|
||||
|
@ -148,7 +149,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do
|
|||
end
|
||||
|
||||
it 'returns nil if valid feed_token and disabled' do
|
||||
allow(Gitlab::CurrentSettings).to receive(:disable_feed_token).and_return(true)
|
||||
stub_application_setting(disable_feed_token: true)
|
||||
set_param(:feed_token, user.feed_token)
|
||||
|
||||
expect(find_user_from_feed_token(:rss)).to be_nil
|
||||
|
@ -166,7 +167,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do
|
|||
end
|
||||
|
||||
context 'when rss_token param is provided' do
|
||||
it 'returns user if valid rssd_token' do
|
||||
it 'returns user if valid rss_token' do
|
||||
set_param(:rss_token, user.feed_token)
|
||||
|
||||
expect(find_user_from_feed_token(:rss)).to eq user
|
||||
|
@ -347,7 +348,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do
|
|||
end
|
||||
|
||||
describe '#find_user_from_access_token' do
|
||||
let(:personal_access_token) { create(:personal_access_token, user: user) }
|
||||
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
|
||||
|
||||
before do
|
||||
set_header('SCRIPT_NAME', 'url.atom')
|
||||
|
@ -386,7 +387,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do
|
|||
end
|
||||
|
||||
context 'when using a non-prefixed access token' do
|
||||
let(:personal_access_token) { create(:personal_access_token, :no_prefix, user: user) }
|
||||
let_it_be(:personal_access_token) { create(:personal_access_token, :no_prefix, user: user) }
|
||||
|
||||
it 'returns user' do
|
||||
set_header('HTTP_AUTHORIZATION', "Bearer #{personal_access_token.token}")
|
||||
|
@ -398,7 +399,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do
|
|||
end
|
||||
|
||||
describe '#find_user_from_web_access_token' do
|
||||
let(:personal_access_token) { create(:personal_access_token, user: user) }
|
||||
let_it_be_with_reload(:personal_access_token) { create(:personal_access_token, user: user) }
|
||||
|
||||
before do
|
||||
set_header(described_class::PRIVATE_TOKEN_HEADER, personal_access_token.token)
|
||||
|
@ -449,6 +450,22 @@ RSpec.describe Gitlab::Auth::AuthFinders do
|
|||
expect(find_user_from_web_access_token(:api)).to be_nil
|
||||
end
|
||||
|
||||
context 'when the token has read_api scope' do
|
||||
before do
|
||||
personal_access_token.update!(scopes: ['read_api'])
|
||||
|
||||
set_header('SCRIPT_NAME', '/api/endpoint')
|
||||
end
|
||||
|
||||
it 'raises InsufficientScopeError by default' do
|
||||
expect { find_user_from_web_access_token(:api) }.to raise_error(Gitlab::Auth::InsufficientScopeError)
|
||||
end
|
||||
|
||||
it 'finds the user when the read_api scope is passed' do
|
||||
expect(find_user_from_web_access_token(:api, scopes: [:api, :read_api])).to eq(user)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when relative_url_root is set' do
|
||||
before do
|
||||
stub_config_setting(relative_url_root: '/relative_root')
|
||||
|
@ -464,7 +481,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do
|
|||
end
|
||||
|
||||
describe '#find_personal_access_token' do
|
||||
let(:personal_access_token) { create(:personal_access_token, user: user) }
|
||||
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
|
||||
|
||||
before do
|
||||
set_header('SCRIPT_NAME', 'url.atom')
|
||||
|
@ -534,7 +551,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do
|
|||
end
|
||||
|
||||
context 'access token is valid' do
|
||||
let(:personal_access_token) { create(:personal_access_token, user: user) }
|
||||
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
|
||||
let(:route_authentication_setting) { { basic_auth_personal_access_token: true } }
|
||||
|
||||
it 'finds the token from basic auth' do
|
||||
|
@ -555,7 +572,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do
|
|||
end
|
||||
|
||||
context 'route_setting is not set' do
|
||||
let(:personal_access_token) { create(:personal_access_token, user: user) }
|
||||
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
|
||||
|
||||
it 'returns nil' do
|
||||
auth_header_with(personal_access_token.token)
|
||||
|
@ -565,7 +582,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do
|
|||
end
|
||||
|
||||
context 'route_setting is not correct' do
|
||||
let(:personal_access_token) { create(:personal_access_token, user: user) }
|
||||
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
|
||||
let(:route_authentication_setting) { { basic_auth_personal_access_token: false } }
|
||||
|
||||
it 'returns nil' do
|
||||
|
@ -611,8 +628,9 @@ RSpec.describe Gitlab::Auth::AuthFinders do
|
|||
|
||||
context 'with CI username' do
|
||||
let(:username) { ::Gitlab::Auth::CI_JOB_USER }
|
||||
let(:user) { create(:user) }
|
||||
let(:build) { create(:ci_build, user: user, status: :running) }
|
||||
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:build) { create(:ci_build, user: user, status: :running) }
|
||||
|
||||
it 'returns nil without password' do
|
||||
set_basic_auth_header(username, nil)
|
||||
|
@ -645,11 +663,11 @@ RSpec.describe Gitlab::Auth::AuthFinders do
|
|||
describe '#validate_access_token!' do
|
||||
subject { validate_access_token! }
|
||||
|
||||
let(:personal_access_token) { create(:personal_access_token, user: user) }
|
||||
let_it_be_with_reload(:personal_access_token) { create(:personal_access_token, user: user) }
|
||||
|
||||
context 'with a job token' do
|
||||
let_it_be(:job) { create(:ci_build, user: user, status: :running) }
|
||||
let(:route_authentication_setting) { { job_token_allowed: true } }
|
||||
let(:job) { create(:ci_build, user: user, status: :running) }
|
||||
|
||||
before do
|
||||
env['HTTP_AUTHORIZATION'] = "Bearer #{job.token}"
|
||||
|
@ -671,7 +689,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do
|
|||
end
|
||||
|
||||
it 'returns Gitlab::Auth::ExpiredError if token expired' do
|
||||
personal_access_token.expires_at = 1.day.ago
|
||||
personal_access_token.update!(expires_at: 1.day.ago)
|
||||
|
||||
expect { validate_access_token! }.to raise_error(Gitlab::Auth::ExpiredError)
|
||||
end
|
||||
|
@ -688,7 +706,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do
|
|||
end
|
||||
|
||||
context 'with impersonation token' do
|
||||
let(:personal_access_token) { create(:personal_access_token, :impersonation, user: user) }
|
||||
let_it_be(:personal_access_token) { create(:personal_access_token, :impersonation, user: user) }
|
||||
|
||||
context 'when impersonation is disabled' do
|
||||
before do
|
||||
|
@ -704,7 +722,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do
|
|||
end
|
||||
|
||||
describe '#find_user_from_job_token' do
|
||||
let(:job) { create(:ci_build, user: user, status: :running) }
|
||||
let_it_be(:job) { create(:ci_build, user: user, status: :running) }
|
||||
let(:route_authentication_setting) { { job_token_allowed: true } }
|
||||
|
||||
subject { find_user_from_job_token }
|
||||
|
@ -866,7 +884,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do
|
|||
end
|
||||
|
||||
describe '#find_runner_from_token' do
|
||||
let(:runner) { create(:ci_runner) }
|
||||
let_it_be(:runner) { create(:ci_runner) }
|
||||
|
||||
context 'with API requests' do
|
||||
before do
|
||||
|
|
|
@ -47,7 +47,10 @@ RSpec.describe Gitlab::Auth::RequestAuthenticator do
|
|||
let!(:job_token_user) { build(:user) }
|
||||
|
||||
it 'returns access_token user first' do
|
||||
allow_any_instance_of(described_class).to receive(:find_user_from_web_access_token).and_return(access_token_user)
|
||||
allow_any_instance_of(described_class).to receive(:find_user_from_web_access_token)
|
||||
.with(anything, scopes: [:api, :read_api])
|
||||
.and_return(access_token_user)
|
||||
|
||||
allow_any_instance_of(described_class).to receive(:find_user_from_feed_token).and_return(feed_token_user)
|
||||
|
||||
expect(subject.find_sessionless_user(:api)).to eq access_token_user
|
||||
|
|
|
@ -47,6 +47,10 @@ RSpec.describe Gitlab::Ci::Charts do
|
|||
|
||||
subject { chart.to }
|
||||
|
||||
before do
|
||||
create(:ci_empty_pipeline, project: project, duration: 120)
|
||||
end
|
||||
|
||||
it 'includes the whole current day' do
|
||||
is_expected.to eq(Date.today.end_of_day)
|
||||
end
|
||||
|
@ -58,6 +62,37 @@ RSpec.describe Gitlab::Ci::Charts do
|
|||
it 'uses %d %B as labels format' do
|
||||
expect(chart.labels).to include(chart.from.strftime('%d %B'))
|
||||
end
|
||||
|
||||
it 'returns count of pipelines run each day in the current week' do
|
||||
expect(chart.total).to contain_exactly(0, 0, 0, 0, 0, 0, 0, 1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'weekchart_non_utc' do
|
||||
today = Date.today
|
||||
end_of_today = Time.use_zone(Time.find_zone('Asia/Dubai')) { today.end_of_day }
|
||||
|
||||
let(:project) { create(:project) }
|
||||
let(:chart) do
|
||||
allow(Date).to receive(:today).and_return(today)
|
||||
allow(today).to receive(:end_of_day).and_return(end_of_today)
|
||||
Gitlab::Ci::Charts::WeekChart.new(project)
|
||||
end
|
||||
|
||||
subject { chart.total }
|
||||
|
||||
before do
|
||||
create(:ci_empty_pipeline, project: project, duration: 120)
|
||||
end
|
||||
|
||||
it 'uses a non-utc time zone for range times' do
|
||||
expect(chart.to.zone).to eq(end_of_today.zone)
|
||||
expect(chart.from.zone).to eq(end_of_today.zone)
|
||||
end
|
||||
|
||||
it 'returns count of pipelines run each day in the current week' do
|
||||
is_expected.to contain_exactly(0, 0, 0, 0, 0, 0, 0, 1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'pipeline_times' do
|
||||
|
|
|
@ -188,10 +188,10 @@ RSpec.describe 'Rack Attack global throttles' do
|
|||
end
|
||||
|
||||
describe 'API requests authenticated with personal access token', :api do
|
||||
let(:user) { create(:user) }
|
||||
let(:token) { create(:personal_access_token, user: user) }
|
||||
let(:other_user) { create(:user) }
|
||||
let(:other_user_token) { create(:personal_access_token, user: other_user) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:token) { create(:personal_access_token, user: user) }
|
||||
let_it_be(:other_user) { create(:user) }
|
||||
let_it_be(:other_user_token) { create(:personal_access_token, user: other_user) }
|
||||
let(:throttle_setting_prefix) { 'throttle_authenticated_api' }
|
||||
let(:api_partial_url) { '/todos' }
|
||||
|
||||
|
@ -208,6 +208,41 @@ RSpec.describe 'Rack Attack global throttles' do
|
|||
|
||||
it_behaves_like 'rate-limited token-authenticated requests'
|
||||
end
|
||||
|
||||
context 'with the token in the OAuth headers' do
|
||||
let(:request_args) { api_get_args_with_token_headers(api_partial_url, oauth_token_headers(token)) }
|
||||
let(:other_user_request_args) { api_get_args_with_token_headers(api_partial_url, oauth_token_headers(other_user_token)) }
|
||||
|
||||
it_behaves_like 'rate-limited token-authenticated requests'
|
||||
end
|
||||
|
||||
context 'with the token in basic auth' do
|
||||
let(:request_args) { api_get_args_with_token_headers(api_partial_url, basic_auth_headers(user, token)) }
|
||||
let(:other_user_request_args) { api_get_args_with_token_headers(api_partial_url, basic_auth_headers(other_user, other_user_token)) }
|
||||
|
||||
it_behaves_like 'rate-limited token-authenticated requests'
|
||||
end
|
||||
|
||||
context 'with a read_api scope' do
|
||||
before do
|
||||
token.update!(scopes: ['read_api'])
|
||||
other_user_token.update!(scopes: ['read_api'])
|
||||
end
|
||||
|
||||
context 'with the token in the headers' do
|
||||
let(:request_args) { api_get_args_with_token_headers(api_partial_url, personal_access_token_headers(token)) }
|
||||
let(:other_user_request_args) { api_get_args_with_token_headers(api_partial_url, personal_access_token_headers(other_user_token)) }
|
||||
|
||||
it_behaves_like 'rate-limited token-authenticated requests'
|
||||
end
|
||||
|
||||
context 'with the token in the OAuth headers' do
|
||||
let(:request_args) { api_get_args_with_token_headers(api_partial_url, oauth_token_headers(token)) }
|
||||
let(:other_user_request_args) { api_get_args_with_token_headers(api_partial_url, oauth_token_headers(other_user_token)) }
|
||||
|
||||
it_behaves_like 'rate-limited token-authenticated requests'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'API requests authenticated with OAuth token', :api do
|
||||
|
@ -235,6 +270,15 @@ RSpec.describe 'Rack Attack global throttles' do
|
|||
|
||||
it_behaves_like 'rate-limited token-authenticated requests'
|
||||
end
|
||||
|
||||
context 'with a read_api scope' do
|
||||
let(:read_token) { Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "read_api") }
|
||||
let(:other_user_read_token) { Doorkeeper::AccessToken.create!(application_id: other_user_application.id, resource_owner_id: other_user.id, scopes: "read_api") }
|
||||
let(:request_args) { api_get_args_with_token_headers(api_partial_url, oauth_token_headers(read_token)) }
|
||||
let(:other_user_request_args) { api_get_args_with_token_headers(api_partial_url, oauth_token_headers(other_user_read_token)) }
|
||||
|
||||
it_behaves_like 'rate-limited token-authenticated requests'
|
||||
end
|
||||
end
|
||||
|
||||
describe '"web" (non-API) requests authenticated with RSS token' do
|
||||
|
|
|
@ -21,6 +21,11 @@ module RackAttackSpecHelpers
|
|||
{ 'AUTHORIZATION' => "Bearer #{oauth_access_token.token}" }
|
||||
end
|
||||
|
||||
def basic_auth_headers(user, personal_access_token)
|
||||
encoded_login = ["#{user.username}:#{personal_access_token.token}"].pack('m0')
|
||||
{ 'AUTHORIZATION' => "Basic #{encoded_login}" }
|
||||
end
|
||||
|
||||
def expect_rejection(&block)
|
||||
yield
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples 'default whitelist' do
|
||||
it 'sanitizes tags that are not whitelisted' do
|
||||
RSpec.shared_examples 'default allowlist' do
|
||||
it 'sanitizes tags that are not allowed' do
|
||||
act = %q{<textarea>no inputs</textarea> and <blink>no blinks</blink>}
|
||||
exp = 'no inputs and no blinks'
|
||||
expect(filter(act).to_html).to eq exp
|
||||
|
|
|
@ -12431,10 +12431,10 @@ vue-jest@4.0.0-rc.0:
|
|||
source-map "0.5.6"
|
||||
ts-jest "26.x"
|
||||
|
||||
vue-loader@^15.9.5:
|
||||
version "15.9.5"
|
||||
resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.9.5.tgz#7a960dc420a3439deaacdda038fdcdbf7c432706"
|
||||
integrity sha512-oeMOs2b5o5gRqkxfds10bCx6JeXYTwivRgbb8hzOrcThD2z1+GqEKE3EX9A2SGbsYDf4rXwRg6D5n1w0jO5SwA==
|
||||
vue-loader@^15.9.6:
|
||||
version "15.9.6"
|
||||
resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.9.6.tgz#f4bb9ae20c3a8370af3ecf09b8126d38ffdb6b8b"
|
||||
integrity sha512-j0cqiLzwbeImIC6nVIby2o/ABAWhlppyL/m5oJ67R5MloP0hj/DtFgb0Zmq3J9CG7AJ+AXIvHVnJAPBvrLyuDg==
|
||||
dependencies:
|
||||
"@vue/component-compiler-utils" "^3.1.0"
|
||||
hash-sum "^1.0.2"
|
||||
|
|
Loading…
Reference in a new issue