New upstream version 13.10.4+ds1

This commit is contained in:
Pirate Praveen 2021-04-28 17:22:55 +05:30
parent 3cf1e9a2bb
commit 941bf6661b
28 changed files with 244 additions and 49 deletions

View file

@ -2,6 +2,18 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 13.10.4 (2021-04-27)
### Security (6 changes)
- Prevent tokens with only read_api scope from executing mutations.
- Update mermaid to version 8.9.2.
- Do not allow deploy tokens in the dependency proxy authentication service.
- Disable keyset pagination for branches by default.
- Bump Carrierwave gem to v1.3.2.
- Restrict setting system_note_timestamp to owners.
## 13.10.3 (2021-04-13) ## 13.10.3 (2021-04-13)
### Security (3 changes) ### Security (3 changes)

View file

@ -1 +1 @@
13.10.3 13.10.4

View file

@ -170,10 +170,11 @@ GEM
capybara-screenshot (1.0.22) capybara-screenshot (1.0.22)
capybara (>= 1.0, < 4) capybara (>= 1.0, < 4)
launchy launchy
carrierwave (1.3.1) carrierwave (1.3.2)
activemodel (>= 4.0.0) activemodel (>= 4.0.0)
activesupport (>= 4.0.0) activesupport (>= 4.0.0)
mime-types (>= 1.16) mime-types (>= 1.16)
ssrf_filter (~> 1.0)
cbor (0.5.9.6) cbor (0.5.9.6)
character_set (1.4.0) character_set (1.4.0)
charlock_holmes (0.7.7) charlock_holmes (0.7.7)
@ -1203,6 +1204,7 @@ GEM
sprockets (>= 3.0.0) sprockets (>= 3.0.0)
sqlite3 (1.3.13) sqlite3 (1.3.13)
sshkey (2.0.0) sshkey (2.0.0)
ssrf_filter (1.0.7)
stackprof (0.2.15) stackprof (0.2.15)
state_machines (0.5.0) state_machines (0.5.0)
state_machines-activemodel (0.8.0) state_machines-activemodel (0.8.0)

View file

@ -1 +1 @@
13.10.3 13.10.4

View file

@ -7,11 +7,15 @@
module SessionlessAuthentication module SessionlessAuthentication
# This filter handles personal access tokens, atom requests with rss tokens, and static object tokens # This filter handles personal access tokens, atom requests with rss tokens, and static object tokens
def authenticate_sessionless_user!(request_format) def authenticate_sessionless_user!(request_format)
user = Gitlab::Auth::RequestAuthenticator.new(request).find_sessionless_user(request_format) user = request_authenticator.find_sessionless_user(request_format)
sessionless_sign_in(user) if user sessionless_sign_in(user) if user
end end
def request_authenticator
@request_authenticator ||= Gitlab::Auth::RequestAuthenticator.new(request)
end
def sessionless_user? def sessionless_user?
current_user && !session.key?('warden.user.user.key') current_user && !session.key?('warden.user.user.key')
end end

View file

@ -108,7 +108,13 @@ class GraphqlController < ApplicationController
end end
def context def context
@context ||= { current_user: current_user, is_sessionless_user: !!sessionless_user?, request: request } api_user = !!sessionless_user?
@context ||= {
current_user: current_user,
is_sessionless_user: api_user,
request: request,
scope_validator: ::Gitlab::Auth::ScopeValidator.new(api_user, request_authenticator)
}
end end
def build_variables(variable_info) def build_variables(variable_info)

View file

@ -185,7 +185,7 @@ class Projects::BranchesController < Projects::ApplicationController
# Here we get one more branch to indicate if there are more data we're not showing # Here we get one more branch to indicate if there are more data we're not showing
limit = @overview_max_branches + 1 limit = @overview_max_branches + 1
if Feature.enabled?(:branch_list_keyset_pagination, project, default_enabled: true) if Feature.enabled?(:branch_list_keyset_pagination, project, default_enabled: :yaml)
@active_branches = @active_branches =
BranchesFinder.new(@repository, { per_page: limit, sort: sort_value_recently_updated }) BranchesFinder.new(@repository, { per_page: limit, sort: sort_value_recently_updated })
.execute(gitaly_pagination: true).select(&:active?) .execute(gitaly_pagination: true).select(&:active?)

View file

@ -28,8 +28,12 @@ module Mutations
end end
def ready?(**args) def ready?(**args)
auth = ::Gitlab::Graphql::Authorize::ObjectAuthorization.new(:execute_graphql_mutation, :api)
if Gitlab::Database.read_only? if Gitlab::Database.read_only?
raise Gitlab::Graphql::Errors::ResourceNotAvailable, ERROR_MESSAGE raise Gitlab::Graphql::Errors::ResourceNotAvailable, ERROR_MESSAGE
elsif !auth.ok?(:global, current_user, scope_validator: context[:scope_validator])
raise_resource_not_available_error!
else else
true true
end end

View file

@ -23,6 +23,7 @@ class GlobalPolicy < BasePolicy
prevent :receive_notifications prevent :receive_notifications
prevent :use_quick_actions prevent :use_quick_actions
prevent :create_group prevent :create_group
prevent :execute_graphql_mutation
end end
rule { default }.policy do rule { default }.policy do
@ -32,6 +33,7 @@ class GlobalPolicy < BasePolicy
enable :receive_notifications enable :receive_notifications
enable :use_quick_actions enable :use_quick_actions
enable :use_slash_commands enable :use_slash_commands
enable :execute_graphql_mutation
end end
rule { inactive }.policy do rule { inactive }.policy do
@ -48,6 +50,8 @@ class GlobalPolicy < BasePolicy
prevent :use_slash_commands prevent :use_slash_commands
end end
rule { ~can?(:access_api) }.prevent :execute_graphql_mutation
rule { blocked | (internal & ~migration_bot & ~security_bot) }.policy do rule { blocked | (internal & ~migration_bot & ~security_bot) }.policy do
prevent :access_git prevent :access_git
end end

View file

@ -8,7 +8,10 @@ module Auth
def execute(authentication_abilities:) def execute(authentication_abilities:)
return error('dependency proxy not enabled', 404) unless ::Gitlab.config.dependency_proxy.enabled return error('dependency proxy not enabled', 404) unless ::Gitlab.config.dependency_proxy.enabled
return error('access forbidden', 403) unless current_user
# Because app/controllers/concerns/dependency_proxy/auth.rb consumes this
# JWT only as `User.find`, we currently only allow User (not DeployToken, etc)
return error('access forbidden', 403) unless current_user.is_a?(User)
{ token: authorized_token.encoded } { token: authorized_token.encoded }
end end

View file

@ -34,7 +34,7 @@ module Issues
private private
def filter_params(merge_request) def filter_params(issue)
super super
moved_issue = params.delete(:moved_issue) moved_issue = params.delete(:moved_issue)
@ -44,6 +44,8 @@ module Issues
params.delete(:iid) unless current_user.can?(:set_issue_iid, project) params.delete(:iid) unless current_user.can?(:set_issue_iid, project)
params.delete(:created_at) unless moved_issue || current_user.can?(:set_issue_created_at, project) params.delete(:created_at) unless moved_issue || current_user.can?(:set_issue_created_at, project)
params.delete(:updated_at) unless moved_issue || current_user.can?(:set_issue_updated_at, project) params.delete(:updated_at) unless moved_issue || current_user.can?(:set_issue_updated_at, project)
issue.system_note_timestamp = params[:created_at] || params[:updated_at]
end end
def create_assignee_note(issue, old_assignees) def create_assignee_note(issue, old_assignees)

View file

@ -37,7 +37,7 @@ class Projects::BranchesByModeService
def use_gitaly_pagination? def use_gitaly_pagination?
return false if params[:page].present? || params[:search].present? return false if params[:page].present? || params[:search].present?
Feature.enabled?(:branch_list_keyset_pagination, project, default_enabled: true) Feature.enabled?(:branch_list_keyset_pagination, project, default_enabled: :yaml)
end end
def fetch_branches_via_offset_pagination def fetch_branches_via_offset_pagination

View file

@ -13,4 +13,4 @@
.form-group .form-group
.well-password-auth.collapse.js-well-password-auth .well-password-auth.collapse.js-well-password-auth
= f.label :password, _("Password"), class: "label-bold" = f.label :password, _("Password"), class: "label-bold"
= f.password_field :password, value: mirror.password, class: 'form-control gl-form-input qa-password', autocomplete: 'new-password' = f.password_field :password, class: 'form-control gl-form-input qa-password', autocomplete: 'new-password'

View file

@ -5,4 +5,4 @@ rollout_issue_url:
milestone: '13.2' milestone: '13.2'
type: development type: development
group: group::source code group: group::source code
default_enabled: true default_enabled: false

View file

@ -249,7 +249,6 @@ module API
authorize! :create_issue, user_project authorize! :create_issue, user_project
issue_params = declared_params(include_missing: false) issue_params = declared_params(include_missing: false)
issue_params[:system_note_timestamp] = params[:created_at]
issue_params = convert_parameters_from_legacy_format(issue_params) issue_params = convert_parameters_from_legacy_format(issue_params)
@ -293,8 +292,6 @@ module API
issue = user_project.issues.find_by!(iid: params.delete(:issue_iid)) issue = user_project.issues.find_by!(iid: params.delete(:issue_iid))
authorize! :update_issue, issue authorize! :update_issue, issue
issue.system_note_timestamp = params[:updated_at]
update_params = declared_params(include_missing: false).merge(request: request, api: true) update_params = declared_params(include_missing: false).merge(request: request, api: true)
update_params = convert_parameters_from_legacy_format(update_params) update_params = convert_parameters_from_legacy_format(update_params)

View file

@ -0,0 +1,24 @@
# frozen_string_literal: true
# Wrapper around a RequestAuthenticator to
# perform authorization of scopes. Access is limited to
# only those methods needed to validate that an API user
# has at least one permitted scope.
module Gitlab
module Auth
class ScopeValidator
def initialize(api_user, request_authenticator)
@api_user = api_user
@request_authenticator = request_authenticator
end
def valid_for?(permitted)
return true unless @api_user
return true if permitted.none?
scopes = permitted.map { |s| API::Scope.new(s) }
@request_authenticator.valid_access_token?(scopes: scopes)
end
end
end
end

View file

@ -0,0 +1,45 @@
# frozen_string_literal: true
module Gitlab
module Graphql
module Authorize
class ObjectAuthorization
attr_reader :abilities, :permitted_scopes
def initialize(abilities, scopes = %i[api read_api])
@abilities = Array.wrap(abilities).flatten
@permitted_scopes = Array.wrap(scopes)
end
def none?
abilities.empty?
end
def any?
abilities.present?
end
def ok?(object, current_user, scope_validator: nil)
scopes_ok?(scope_validator) && abilities_ok?(object, current_user)
end
private
def abilities_ok?(object, current_user)
return true if none?
subject = object.try(:declarative_policy_subject) || object
abilities.all? do |ability|
Ability.allowed?(current_user, ability, subject)
end
end
def scopes_ok?(validator)
return true unless validator.present?
validator.valid_for?(permitted_scopes)
end
end
end
end
end

View file

@ -26,11 +26,11 @@ module Gitlab
private private
def keyset_pagination_enabled? def keyset_pagination_enabled?
Feature.enabled?(:branch_list_keyset_pagination, project, default_enabled: true) && params[:pagination] == 'keyset' Feature.enabled?(:branch_list_keyset_pagination, project, default_enabled: :yaml) && params[:pagination] == 'keyset'
end end
def paginate_first_page? def paginate_first_page?
Feature.enabled?(:branch_list_keyset_pagination, project, default_enabled: true) && (params[:page].blank? || params[:page].to_i == 1) Feature.enabled?(:branch_list_keyset_pagination, project, default_enabled: :yaml) && (params[:page].blank? || params[:page].to_i == 1)
end end
def paginate_via_gitaly(finder) def paginate_via_gitaly(finder)

View file

@ -121,7 +121,7 @@
"lodash": "^4.17.20", "lodash": "^4.17.20",
"marked": "^0.3.12", "marked": "^0.3.12",
"mathjax": "3", "mathjax": "3",
"mermaid": "^8.9.0", "mermaid": "^8.9.2",
"minimatch": "^3.0.4", "minimatch": "^3.0.4",
"monaco-editor": "^0.20.0", "monaco-editor": "^0.20.0",
"monaco-editor-webpack-plugin": "^1.9.0", "monaco-editor-webpack-plugin": "^1.9.0",

View file

@ -31,6 +31,8 @@ RSpec.describe 'Adding a Note' do
project.add_developer(current_user) project.add_developer(current_user)
end end
it_behaves_like 'a working GraphQL mutation'
it_behaves_like 'a Note mutation that creates a Note' it_behaves_like 'a Note mutation that creates a Note'
it_behaves_like 'a Note mutation when there are active record validation errors' it_behaves_like 'a Note mutation when there are active record validation errors'

View file

@ -943,6 +943,34 @@ RSpec.describe API::Issues do
it_behaves_like 'issuable update endpoint' do it_behaves_like 'issuable update endpoint' do
let(:entity) { issue } let(:entity) { issue }
end end
describe 'updated_at param' do
let(:fixed_time) { Time.new(2001, 1, 1) }
let(:updated_at) { Time.new(2000, 1, 1) }
before do
travel_to fixed_time
end
it 'allows admins to set the timestamp' do
put api("/projects/#{project.id}/issues/#{issue.iid}", admin), params: { labels: 'label1', updated_at: updated_at }
expect(response).to have_gitlab_http_status(:ok)
expect(Time.parse(json_response['updated_at'])).to be_like_time(updated_at)
expect(ResourceLabelEvent.last.created_at).to be_like_time(updated_at)
end
it 'does not allow other users to set the timestamp' do
reporter = create(:user)
project.add_developer(reporter)
put api("/projects/#{project.id}/issues/#{issue.iid}", reporter), params: { labels: 'label1', updated_at: updated_at }
expect(response).to have_gitlab_http_status(:ok)
expect(Time.parse(json_response['updated_at'])).to be_like_time(fixed_time)
expect(ResourceLabelEvent.last.created_at).to be_like_time(fixed_time)
end
end
end end
describe 'DELETE /projects/:id/issues/:issue_iid' do describe 'DELETE /projects/:id/issues/:issue_iid' do

View file

@ -330,15 +330,21 @@ RSpec.describe API::Issues do
end end
context 'setting created_at' do context 'setting created_at' do
let(:fixed_time) { Time.new(2001, 1, 1) }
let(:creation_time) { 2.weeks.ago } let(:creation_time) { 2.weeks.ago }
let(:params) { { title: 'new issue', labels: 'label, label2', created_at: creation_time } } let(:params) { { title: 'new issue', labels: 'label, label2', created_at: creation_time } }
before do
travel_to fixed_time
end
context 'by an admin' do context 'by an admin' do
it 'sets the creation time on the new issue' do it 'sets the creation time on the new issue' do
post api("/projects/#{project.id}/issues", admin), params: params post api("/projects/#{project.id}/issues", admin), params: params
expect(response).to have_gitlab_http_status(:created) expect(response).to have_gitlab_http_status(:created)
expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time) expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time)
expect(ResourceLabelEvent.last.created_at).to be_like_time(creation_time)
end end
end end
@ -348,6 +354,7 @@ RSpec.describe API::Issues do
expect(response).to have_gitlab_http_status(:created) expect(response).to have_gitlab_http_status(:created)
expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time) expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time)
expect(ResourceLabelEvent.last.created_at).to be_like_time(creation_time)
end end
end end
@ -356,19 +363,24 @@ RSpec.describe API::Issues do
group = create(:group) group = create(:group)
group_project = create(:project, :public, namespace: group) group_project = create(:project, :public, namespace: group)
group.add_owner(user2) group.add_owner(user2)
post api("/projects/#{group_project.id}/issues", user2), params: params post api("/projects/#{group_project.id}/issues", user2), params: params
expect(response).to have_gitlab_http_status(:created) expect(response).to have_gitlab_http_status(:created)
expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time) expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time)
expect(ResourceLabelEvent.last.created_at).to be_like_time(creation_time)
end end
end end
context 'by another user' do context 'by another user' do
it 'ignores the given creation time' do it 'ignores the given creation time' do
project.add_developer(user2)
post api("/projects/#{project.id}/issues", user2), params: params post api("/projects/#{project.id}/issues", user2), params: params
expect(response).to have_gitlab_http_status(:created) expect(response).to have_gitlab_http_status(:created)
expect(Time.parse(json_response['created_at'])).not_to be_like_time(creation_time) expect(Time.parse(json_response['created_at'])).to be_like_time(fixed_time)
expect(ResourceLabelEvent.last.created_at).to be_like_time(fixed_time)
end end
end end
end end

View file

@ -262,25 +262,21 @@ RSpec.describe JwtController do
let(:credential_user) { group_deploy_token.username } let(:credential_user) { group_deploy_token.username }
let(:credential_password) { group_deploy_token.token } let(:credential_password) { group_deploy_token.token }
it_behaves_like 'with valid credentials' it_behaves_like 'returning response status', :forbidden
end end
context 'with project deploy token' do context 'with project deploy token' do
let(:credential_user) { project_deploy_token.username } let(:credential_user) { project_deploy_token.username }
let(:credential_password) { project_deploy_token.token } let(:credential_password) { project_deploy_token.token }
it_behaves_like 'with valid credentials' it_behaves_like 'returning response status', :forbidden
end end
context 'with invalid credentials' do context 'with invalid credentials' do
let(:credential_user) { 'foo' } let(:credential_user) { 'foo' }
let(:credential_password) { 'bar' } let(:credential_password) { 'bar' }
it 'returns unauthorized' do it_behaves_like 'returning response status', :unauthorized
subject
expect(response).to have_gitlab_http_status(:unauthorized)
end
end end
end end

View file

@ -13,28 +13,31 @@ RSpec.describe Auth::DependencyProxyAuthenticationService do
describe '#execute' do describe '#execute' do
subject { service.execute(authentication_abilities: nil) } subject { service.execute(authentication_abilities: nil) }
shared_examples 'returning' do |status:, message:|
it "returns #{message}", :aggregate_failures do
expect(subject[:http_status]).to eq(status)
expect(subject[:message]).to eq(message)
end
end
context 'dependency proxy is not enabled' do context 'dependency proxy is not enabled' do
before do before do
stub_config(dependency_proxy: { enabled: false }) stub_config(dependency_proxy: { enabled: false })
end end
it 'returns not found' do it_behaves_like 'returning', status: 404, message: 'dependency proxy not enabled'
result = subject
expect(result[:http_status]).to eq(404)
expect(result[:message]).to eq('dependency proxy not enabled')
end
end end
context 'without a user' do context 'without a user' do
let(:user) { nil } let(:user) { nil }
it 'returns forbidden' do it_behaves_like 'returning', status: 403, message: 'access forbidden'
result = subject end
expect(result[:http_status]).to eq(403) context 'with a deploy token as user' do
expect(result[:message]).to eq('access forbidden') let_it_be(:user) { create(:deploy_token) }
end
it_behaves_like 'returning', status: 403, message: 'access forbidden'
end end
context 'with a user' do context 'with a user' do

View file

@ -20,8 +20,9 @@ RSpec.describe Projects::DownloadService do
context 'for URLs that are on the whitelist' do context 'for URLs that are on the whitelist' do
before do before do
stub_request(:get, 'http://mycompany.fogbugz.com/rails_sample.jpg').to_return(body: File.read(Rails.root + 'spec/fixtures/rails_sample.jpg')) # `ssrf_filter` resolves the hostname. See https://github.com/carrierwaveuploader/carrierwave/commit/91714adda998bc9e8decf5b1f5d260d808761304
stub_request(:get, 'http://mycompany.fogbugz.com/doc_sample.txt').to_return(body: File.read(Rails.root + 'spec/fixtures/doc_sample.txt')) stub_request(:get, %r{http://[\d\.]+/rails_sample.jpg}).to_return(body: File.read(Rails.root + 'spec/fixtures/rails_sample.jpg'))
stub_request(:get, %r{http://[\d\.]+/doc_sample.txt}).to_return(body: File.read(Rails.root + 'spec/fixtures/doc_sample.txt'))
end end
context 'an image file' do context 'an image file' do

View file

@ -390,17 +390,21 @@ module GraphqlHelpers
post api('/', current_user, version: 'graphql'), params: { _json: queries }, headers: headers post api('/', current_user, version: 'graphql'), params: { _json: queries }, headers: headers
end end
def post_graphql(query, current_user: nil, variables: nil, headers: {}) def post_graphql(query, current_user: nil, variables: nil, headers: {}, token: {})
params = { query: query, variables: serialize_variables(variables) } params = { query: query, variables: serialize_variables(variables) }
post api('/', current_user, version: 'graphql'), params: params, headers: headers post api('/', current_user, version: 'graphql', **token), params: params, headers: headers
if graphql_errors # Errors are acceptable, but not this one: return unless graphql_errors
expect(graphql_errors).not_to include(a_hash_including('message' => 'Internal server error'))
end # Errors are acceptable, but not this one:
expect(graphql_errors).not_to include(a_hash_including('message' => 'Internal server error'))
end end
def post_graphql_mutation(mutation, current_user: nil) def post_graphql_mutation(mutation, current_user: nil, token: {})
post_graphql(mutation.query, current_user: current_user, variables: mutation.variables) post_graphql(mutation.query,
current_user: current_user,
variables: mutation.variables,
token: token)
end end
def post_graphql_mutation_with_uploads(mutation, current_user: nil) def post_graphql_mutation_with_uploads(mutation, current_user: nil)

View file

@ -10,6 +10,52 @@ RSpec.shared_examples 'a working graphql query' do
end end
end end
RSpec.shared_examples 'a working GraphQL mutation' do
include GraphqlHelpers
before do
post_graphql_mutation(mutation, current_user: current_user, token: token)
end
shared_examples 'allows access to the mutation' do
let(:scopes) { ['api'] }
it_behaves_like 'a working graphql query' do
it 'returns data' do
expect(graphql_data.compact).not_to be_empty
end
end
end
shared_examples 'prevents access to the mutation' do
let(:scopes) { ['read_api'] }
it 'does not resolve the mutation' do
expect(graphql_data.compact).to be_empty
expect(graphql_errors).to be_present
end
end
context 'with a personal access token' do
let(:token) do
pat = create(:personal_access_token, user: current_user, scopes: scopes)
{ personal_access_token: pat }
end
it_behaves_like 'prevents access to the mutation'
it_behaves_like 'allows access to the mutation'
end
context 'with an OAuth token' do
let(:token) do
{ oauth_access_token: create(:oauth_access_token, resource_owner: current_user, scopes: scopes.join(' ')) }
end
it_behaves_like 'prevents access to the mutation'
it_behaves_like 'allows access to the mutation'
end
end
RSpec.shared_examples 'a mutation on an unauthorized resource' do RSpec.shared_examples 'a mutation on an unauthorized resource' do
it_behaves_like 'a mutation that returns top-level errors', it_behaves_like 'a mutation that returns top-level errors',
errors: [::Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] errors: [::Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]

View file

@ -8194,10 +8194,10 @@ merge2@^1.3.0:
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
mermaid@^8.9.0: mermaid@^8.9.2:
version "8.9.0" version "8.9.2"
resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-8.9.0.tgz#e569517863ab903aa5389cd746b68ca958a8ca7c" resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-8.9.2.tgz#40bb2052cc6c4feaf5d93a5e527a8d06d0bacea7"
integrity sha512-J582tyE1vkdNu4BGgfwXnFo4Mu6jpuc4uK96mIenavaak9kr4T5gaMmYCo/7edwq/vTBkx/soZ5LcJo5WXZ1BQ== integrity sha512-XWEaraDRDlHZexdeHSSr/MH4VJAOksRSPudchi69ecZJ7IUjjlzHsg32n4ZwJUh6lFO+NMYLHwHNNYUyxIjGPg==
dependencies: dependencies:
"@braintree/sanitize-url" "^3.1.0" "@braintree/sanitize-url" "^3.1.0"
d3 "^5.7.0" d3 "^5.7.0"