Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new Gemspec/RequireMFA cop #10243

Merged
merged 2 commits into from Nov 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/new_add_new_gemspecrequiremfa_cop.md
@@ -0,0 +1 @@
* [#10243](https://github.com/rubocop/rubocop/pull/10243): Add new `Gemspec/RequireMFA` cop. ([@dvandersluis][])
9 changes: 9 additions & 0 deletions config/default.yml
Expand Up @@ -259,6 +259,15 @@ Gemspec/OrderedDependencies:
Include:
- '**/*.gemspec'

Gemspec/RequireMFA:
Description: 'Checks that the gemspec has metadata to require MFA from RubyGems.'
Enabled: pending
VersionAdded: '<<next>>'
Reference:
- https://guides.rubygems.org/mfa-requirement-opt-in/
Include:
- '**/*.gemspec'

Gemspec/RequiredRubyVersion:
Description: 'Checks that `required_ruby_version` of gemspec is specified and equal to `TargetRubyVersion` of .rubocop.yml.'
Enabled: true
Expand Down
2 changes: 2 additions & 0 deletions lib/rubocop.rb
Expand Up @@ -83,6 +83,7 @@
require_relative 'rubocop/cop/mixin/first_element_line_break'
require_relative 'rubocop/cop/mixin/frozen_string_literal'
require_relative 'rubocop/cop/mixin/gem_declaration'
require_relative 'rubocop/cop/mixin/gemspec_help'
require_relative 'rubocop/cop/mixin/hash_alignment_styles'
require_relative 'rubocop/cop/mixin/hash_transform_method'
require_relative 'rubocop/cop/mixin/ignored_pattern'
Expand Down Expand Up @@ -160,6 +161,7 @@
require_relative 'rubocop/cop/gemspec/date_assignment'
require_relative 'rubocop/cop/gemspec/duplicated_assignment'
require_relative 'rubocop/cop/gemspec/ordered_dependencies'
require_relative 'rubocop/cop/gemspec/require_mfa'
require_relative 'rubocop/cop/gemspec/required_ruby_version'
require_relative 'rubocop/cop/gemspec/ruby_version_globals_usage'

Expand Down
12 changes: 2 additions & 10 deletions lib/rubocop/cop/gemspec/date_assignment.rb
Expand Up @@ -21,21 +21,13 @@ module Gemspec
#
class DateAssignment < Base
include RangeHelp
include GemspecHelp
extend AutoCorrector

MSG = 'Do not use `date =` in gemspec, it is set automatically when the gem is packaged.'

# @!method gem_specification(node)
def_node_matcher :gem_specification, <<~PATTERN
(block
(send
(const
(const {cbase nil?} :Gem) :Specification) :new)
...)
PATTERN

def on_block(block_node)
return unless gem_specification(block_node)
return unless gem_specification?(block_node)

block_parameter = block_node.arguments.first.source

Expand Down
11 changes: 1 addition & 10 deletions lib/rubocop/cop/gemspec/duplicated_assignment.rb
Expand Up @@ -36,20 +36,11 @@ module Gemspec
# end
class DuplicatedAssignment < Base
include RangeHelp
include GemspecHelp

MSG = '`%<assignment>s` method calls already given on line '\
'%<line_of_first_occurrence>d of the gemspec.'

# @!method gem_specification(node)
def_node_search :gem_specification, <<~PATTERN
(block
(send
(const
(const {cbase nil?} :Gem) :Specification) :new)
(args
(arg $_)) ...)
PATTERN

# @!method assignment_method_declarations(node)
def_node_search :assignment_method_declarations, <<~PATTERN
(send
Expand Down
146 changes: 146 additions & 0 deletions lib/rubocop/cop/gemspec/require_mfa.rb
@@ -0,0 +1,146 @@
# frozen_string_literal: true

module RuboCop
module Cop
module Gemspec
# Requires a gemspec to have `rubygems_mfa_required` metadata set.
#
# This setting tells RubyGems that MFA is required for accounts to
# be able perform any of these privileged operations:
#
# * gem push
# * gem yank
# * gem owner --add/remove
# * adding or removing owners using gem ownership page
#
# This helps make your gem more secure, as users can be more
# confident that gem updates were pushed by maintainers.
#
# @example
#
# # bad
# Gem::Specification.new do |spec|
# # no `rubygems_mfa_required` metadata specified
# end
#
# # good
# Gem::Specification.new do |spec|
# spec.metadata = {
# 'rubygems_mfa_required' => 'true'
# }
# end
#
# # good
# Gem::Specification.new do |spec|
# spec.metadata['rubygems_mfa_required'] = 'true'
# end
#
# # bad
# Gem::Specification.new do |spec|
# spec.metadata = {
# 'rubygems_mfa_required' => 'false'
# }
# end
#
# # good
# Gem::Specification.new do |spec|
# spec.metadata = {
# 'rubygems_mfa_required' => 'true'
# }
# end
#
# # bad
# Gem::Specification.new do |spec|
# spec.metadata['rubygems_mfa_required'] = 'false'
# end
#
# # good
# Gem::Specification.new do |spec|
# spec.metadata['rubygems_mfa_required'] = 'true'
# end
#
class RequireMFA < Base
include GemspecHelp
extend AutoCorrector

MSG = "`metadata['rubygems_mfa_required']` must be set to `'true'`."

# @!method metadata(node)
def_node_matcher :metadata, <<~PATTERN
`{
(send _ :metadata= $_)
(send (send _ :metadata) :[]= (str "rubygems_mfa_required") $_)
}
PATTERN

# @!method rubygems_mfa_required(node)
def_node_search :rubygems_mfa_required, <<~PATTERN
(pair (str "rubygems_mfa_required") $_)
PATTERN

# @!method true_string?(node)
def_node_matcher :true_string?, <<~PATTERN
(str "true")
PATTERN

def on_block(node) # rubocop:disable Metrics/MethodLength
gem_specification(node) do |block_var|
metadata_value = metadata(node)
mfa_value = mfa_value(metadata_value)

if mfa_value
unless true_string?(mfa_value)
add_offense(mfa_value) do |corrector|
change_value(corrector, mfa_value)
end
end
else
add_offense(node) do |corrector|
autocorrect(corrector, node, block_var, metadata_value)
end
end
end
end

private

def mfa_value(metadata_value)
return unless metadata_value
return metadata_value if metadata_value.str_type?

rubygems_mfa_required(metadata_value).first
end

def autocorrect(corrector, node, block_var, metadata)
if metadata
return unless metadata.hash_type?

correct_metadata(corrector, metadata)
else
correct_missing_metadata(corrector, node, block_var)
end
end

def correct_metadata(corrector, metadata)
if metadata.pairs.any?
corrector.insert_after(metadata.pairs.last, ",\n'rubygems_mfa_required' => 'true'")
else
corrector.insert_before(metadata.loc.end, "'rubygems_mfa_required' => 'true'")
end
end

def correct_missing_metadata(corrector, node, block_var)
corrector.insert_before(node.loc.end, <<~RUBY)
#{block_var}.metadata = {
'rubygems_mfa_required' => 'true'
}
RUBY
end

def change_value(corrector, value)
corrector.replace(value, "'true'")
end
end
end
end
end
13 changes: 3 additions & 10 deletions lib/rubocop/cop/gemspec/ruby_version_globals_usage.rb
Expand Up @@ -26,20 +26,13 @@ module Gemspec
# end
#
class RubyVersionGlobalsUsage < Base
include GemspecHelp

MSG = 'Do not use `RUBY_VERSION` in gemspec file.'

# @!method ruby_version?(node)
def_node_matcher :ruby_version?, '(const {cbase nil?} :RUBY_VERSION)'

# @!method gem_specification?(node)
def_node_search :gem_specification?, <<~PATTERN
(block
(send
(const
(const {cbase nil?} :Gem) :Specification) :new)
...)
PATTERN

def on_const(node)
return unless gem_spec_with_ruby_version?(node)

Expand All @@ -49,7 +42,7 @@ def on_const(node)
private

def gem_spec_with_ruby_version?(node)
gem_specification?(processed_source.ast) && ruby_version?(node)
gem_specification(processed_source.ast) && ruby_version?(node)
end
end
end
Expand Down
30 changes: 30 additions & 0 deletions lib/rubocop/cop/mixin/gemspec_help.rb
@@ -0,0 +1,30 @@
# frozen_string_literal: true

module RuboCop
module Cop
# Common functionality for checking gem declarations.
module GemspecHelp
extend NodePattern::Macros

# @!method gem_specification?(node)
def_node_matcher :gem_specification?, <<~PATTERN
(block
(send
(const
(const {cbase nil?} :Gem) :Specification) :new)
(args
(arg $_)) ...)
PATTERN

# @!method gem_specification(node)
def_node_search :gem_specification, <<~PATTERN
(block
(send
(const
(const {cbase nil?} :Gem) :Specification) :new)
(args
(arg $_)) ...)
PATTERN
end
end
end