mauth-client 6.4.2 → 7.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +6 -3
- data/.ruby-version +1 -1
- data/.travis.yml +7 -5
- data/Appraisals +1 -1
- data/CHANGELOG.md +11 -0
- data/Gemfile +16 -0
- data/README.md +78 -29
- data/Rakefile +20 -12
- data/UPGRADE_GUIDE.md +21 -0
- data/doc/mauth-client_CLI.md +1 -11
- data/examples/Gemfile +0 -1
- data/examples/README.md +14 -13
- data/examples/get_country_info.rb +44 -0
- data/exe/mauth-client +1 -23
- data/gemfiles/faraday_0.x.gemfile +17 -1
- data/gemfiles/faraday_1.x.gemfile +16 -0
- data/gemfiles/faraday_2.x.gemfile +16 -0
- data/lib/mauth/client/{local_authenticator.rb → authenticator.rb} +124 -3
- data/lib/mauth/client/security_token_cacher.rb +1 -1
- data/lib/mauth/client.rb +13 -99
- data/lib/mauth/config_env.rb +84 -0
- data/lib/mauth/version.rb +1 -1
- data/mauth-client.gemspec +2 -17
- metadata +11 -220
- data/doc/mauth.yml.md +0 -84
- data/examples/Gemfile.lock +0 -69
- data/examples/config.yml +0 -12
- data/examples/get_user_info.rb +0 -58
- data/lib/mauth/client/authenticator_base.rb +0 -133
- data/lib/mauth/client/remote_authenticator.rb +0 -85
- data/lib/mauth/dice_bag/mauth.rb.dice +0 -12
- data/lib/mauth/dice_bag/mauth.yml.dice +0 -18
- data/lib/mauth/dice_bag/mauth_key.dice +0 -1
- data/lib/mauth/dice_bag/mauth_templates.rb +0 -21
@@ -4,14 +4,135 @@ require 'mauth/client/security_token_cacher'
|
|
4
4
|
require 'mauth/client/signer'
|
5
5
|
require 'openssl'
|
6
6
|
|
7
|
-
# methods to verify the authenticity of signed requests and responses
|
8
|
-
# public keys from the mAuth service as needed
|
7
|
+
# methods to verify the authenticity of signed requests and responses
|
9
8
|
|
10
9
|
module MAuth
|
11
10
|
class Client
|
12
|
-
module
|
11
|
+
module Authenticator
|
12
|
+
ALLOWED_DRIFT_SECONDS = 300
|
13
|
+
|
14
|
+
# takes an incoming request or response object, and returns whether
|
15
|
+
# the object is authentic according to its signature.
|
16
|
+
def authentic?(object)
|
17
|
+
log_authentication_request(object)
|
18
|
+
begin
|
19
|
+
authenticate!(object)
|
20
|
+
true
|
21
|
+
rescue InauthenticError, MAuthNotPresent, MissingV2Error
|
22
|
+
false
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# raises InauthenticError unless the given object is authentic. Will only
|
27
|
+
# authenticate with v2 if the environment variable V2_ONLY_AUTHENTICATE
|
28
|
+
# is set. Otherwise will fall back to v1 when v2 authentication fails
|
29
|
+
def authenticate!(object)
|
30
|
+
case object.protocol_version
|
31
|
+
when 2
|
32
|
+
begin
|
33
|
+
authenticate_v2!(object)
|
34
|
+
rescue InauthenticError => e
|
35
|
+
raise e if v2_only_authenticate?
|
36
|
+
raise e if disable_fallback_to_v1_on_v2_failure?
|
37
|
+
|
38
|
+
object.fall_back_to_mws_signature_info
|
39
|
+
raise e unless object.signature
|
40
|
+
|
41
|
+
log_authentication_request(object)
|
42
|
+
authenticate_v1!(object)
|
43
|
+
logger.warn('Completed successful authentication attempt after fallback to v1')
|
44
|
+
end
|
45
|
+
when 1
|
46
|
+
if v2_only_authenticate?
|
47
|
+
# If v2 is required but not present and v1 is present we raise MissingV2Error
|
48
|
+
msg = 'This service requires mAuth v2 mcc-authentication header but only v1 x-mws-authentication is present'
|
49
|
+
logger.error(msg)
|
50
|
+
raise MissingV2Error, msg
|
51
|
+
end
|
52
|
+
|
53
|
+
authenticate_v1!(object)
|
54
|
+
else
|
55
|
+
sub_str = v2_only_authenticate? ? '' : 'X-MWS-Authentication header is blank, '
|
56
|
+
msg = "Authentication Failed. No mAuth signature present; #{sub_str}MCC-Authentication header is blank."
|
57
|
+
logger.warn("mAuth signature not present on #{object.class}. Exception: #{msg}")
|
58
|
+
raise MAuthNotPresent, msg
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
13
62
|
private
|
14
63
|
|
64
|
+
# NOTE: This log is likely consumed downstream and the contents SHOULD NOT
|
65
|
+
# be changed without a thorough review of downstream consumers.
|
66
|
+
def log_authentication_request(object)
|
67
|
+
object_app_uuid = object.signature_app_uuid || '[none provided]'
|
68
|
+
object_token = object.signature_token || '[none provided]'
|
69
|
+
logger.info(
|
70
|
+
'Mauth-client attempting to authenticate request from app with mauth ' \
|
71
|
+
"app uuid #{object_app_uuid} to app with mauth app uuid #{client_app_uuid} " \
|
72
|
+
"using version #{object_token}."
|
73
|
+
)
|
74
|
+
end
|
75
|
+
|
76
|
+
def log_inauthentic(object, message)
|
77
|
+
logger.error("mAuth signature authentication failed for #{object.class}. Exception: #{message}")
|
78
|
+
end
|
79
|
+
|
80
|
+
def time_within_valid_range!(object, time_signed, now = Time.now)
|
81
|
+
return if (-ALLOWED_DRIFT_SECONDS..ALLOWED_DRIFT_SECONDS).cover?(now.to_i - time_signed)
|
82
|
+
|
83
|
+
msg = "Time verification failed. #{time_signed} not within #{ALLOWED_DRIFT_SECONDS} of #{now}"
|
84
|
+
log_inauthentic(object, msg)
|
85
|
+
raise InauthenticError, msg
|
86
|
+
end
|
87
|
+
|
88
|
+
# V1 helpers
|
89
|
+
def authenticate_v1!(object)
|
90
|
+
time_valid_v1!(object)
|
91
|
+
token_valid_v1!(object)
|
92
|
+
signature_valid_v1!(object)
|
93
|
+
end
|
94
|
+
|
95
|
+
def time_valid_v1!(object)
|
96
|
+
if object.x_mws_time.nil?
|
97
|
+
msg = 'Time verification failed. No x-mws-time present.'
|
98
|
+
log_inauthentic(object, msg)
|
99
|
+
raise InauthenticError, msg
|
100
|
+
end
|
101
|
+
time_within_valid_range!(object, object.x_mws_time.to_i)
|
102
|
+
end
|
103
|
+
|
104
|
+
def token_valid_v1!(object)
|
105
|
+
return if object.signature_token == MWS_TOKEN
|
106
|
+
|
107
|
+
msg = "Token verification failed. Expected #{MWS_TOKEN}; token was #{object.signature_token}"
|
108
|
+
log_inauthentic(object, msg)
|
109
|
+
raise InauthenticError, msg
|
110
|
+
end
|
111
|
+
|
112
|
+
# V2 helpers
|
113
|
+
def authenticate_v2!(object)
|
114
|
+
time_valid_v2!(object)
|
115
|
+
token_valid_v2!(object)
|
116
|
+
signature_valid_v2!(object)
|
117
|
+
end
|
118
|
+
|
119
|
+
def time_valid_v2!(object)
|
120
|
+
if object.mcc_time.nil?
|
121
|
+
msg = 'Time verification failed. No MCC-Time present.'
|
122
|
+
log_inauthentic(object, msg)
|
123
|
+
raise InauthenticError, msg
|
124
|
+
end
|
125
|
+
time_within_valid_range!(object, object.mcc_time.to_i)
|
126
|
+
end
|
127
|
+
|
128
|
+
def token_valid_v2!(object)
|
129
|
+
return if object.signature_token == MWSV2_TOKEN
|
130
|
+
|
131
|
+
msg = "Token verification failed. Expected #{MWSV2_TOKEN}; token was #{object.signature_token}"
|
132
|
+
log_inauthentic(object, msg)
|
133
|
+
raise InauthenticError, msg
|
134
|
+
end
|
135
|
+
|
15
136
|
def signature_valid_v1!(object)
|
16
137
|
# We are in an unfortunate situation in which Euresource is percent-encoding parts of paths, but not
|
17
138
|
# all of them. In particular, Euresource is percent-encoding all special characters save for '/'.
|
data/lib/mauth/client.rb
CHANGED
@@ -7,12 +7,10 @@ require 'json'
|
|
7
7
|
require 'yaml'
|
8
8
|
require 'mauth/core_ext'
|
9
9
|
require 'mauth/autoload'
|
10
|
-
require 'mauth/dice_bag/mauth_templates'
|
11
10
|
require 'mauth/version'
|
12
|
-
require 'mauth/client/
|
13
|
-
require 'mauth/client/local_authenticator'
|
14
|
-
require 'mauth/client/remote_authenticator'
|
11
|
+
require 'mauth/client/authenticator'
|
15
12
|
require 'mauth/client/signer'
|
13
|
+
require 'mauth/config_env'
|
16
14
|
require 'mauth/errors'
|
17
15
|
|
18
16
|
module MAuth
|
@@ -31,66 +29,22 @@ module MAuth
|
|
31
29
|
AUTH_HEADER_DELIMITER = ';'
|
32
30
|
RACK_ENV_APP_UUID_KEY = 'mauth.app_uuid'
|
33
31
|
|
34
|
-
include
|
32
|
+
include Authenticator
|
35
33
|
include Signer
|
36
34
|
|
37
35
|
# returns a configuration (to be passed to MAuth::Client.new) which is configured from information stored in
|
38
36
|
# standard places. all of which is overridable by options in case some defaults do not apply.
|
39
37
|
#
|
40
38
|
# options (may be symbols or strings) - any or all may be omitted where your usage conforms to the defaults.
|
41
|
-
# -
|
42
|
-
#
|
43
|
-
#
|
44
|
-
# tries Rails.environment, ENV['RAILS_ENV'], and ENV['RACK_ENV'], and falls back to 'development' if none
|
45
|
-
# of these are set.
|
46
|
-
# - mauth_config - MAuth configuration. defaults to load this from a yaml file (see mauth_config_yml option)
|
47
|
-
# which is assumed to be keyed with the environment at the root. if this is specified, no yaml file is
|
48
|
-
# loaded, and the given config is passed through with any other defaults applied. at the moment, the only
|
49
|
-
# other default is to set the logger.
|
50
|
-
# - mauth_config_yml - specifies where a mauth configuration yaml file can be found. by default checks
|
51
|
-
# ENV['MAUTH_CONFIG_YML'] or a file 'config/mauth.yml' relative to the root.
|
39
|
+
# - mauth_config - MAuth configuration. defaults to load this from environment variables. if this is specified,
|
40
|
+
# no environment variable is loaded, and the given config is passed through with any other defaults applied.
|
41
|
+
# at the moment, the only other default is to set the logger.
|
52
42
|
# - logger - by default checks ::Rails.logger
|
53
43
|
def self.default_config(options = {})
|
54
44
|
options = options.stringify_symbol_keys
|
55
45
|
|
56
|
-
# find
|
57
|
-
|
58
|
-
app_root = options['root'] || begin
|
59
|
-
if Object.const_defined?(:Rails) && ::Rails.respond_to?(:root) && ::Rails.root
|
60
|
-
Rails.root
|
61
|
-
else
|
62
|
-
ENV['RAILS_ROOT'] || ENV['RACK_ROOT'] || ENV['APP_ROOT'] || '.'
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
# find the environment (with which yaml files are keyed)
|
67
|
-
env = options['environment'] || begin
|
68
|
-
if Object.const_defined?(:Rails) && ::Rails.respond_to?(:environment)
|
69
|
-
Rails.environment
|
70
|
-
else
|
71
|
-
ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
|
-
# find mauth config, given on options, or in a file at
|
76
|
-
# ENV['MAUTH_CONFIG_YML'] or config/mauth.yml in the app_root
|
77
|
-
mauth_config = options['mauth_config'] || begin
|
78
|
-
mauth_config_yml = options['mauth_config_yml']
|
79
|
-
mauth_config_yml ||= ENV['MAUTH_CONFIG_YML']
|
80
|
-
default_loc = 'config/mauth.yml'
|
81
|
-
default_yml = File.join(app_root, default_loc)
|
82
|
-
mauth_config_yml ||= default_yml if File.exist?(default_yml)
|
83
|
-
if mauth_config_yml && File.exist?(mauth_config_yml)
|
84
|
-
whole_config = ConfigFile.load(mauth_config_yml)
|
85
|
-
errmessage = "#{mauth_config_yml} config has no key #{env} - it has keys #{whole_config.keys.inspect}"
|
86
|
-
whole_config[env] || raise(MAuth::Client::ConfigurationError, errmessage)
|
87
|
-
else
|
88
|
-
raise MAuth::Client::ConfigurationError,
|
89
|
-
'could not find mauth config yaml file. this file may be ' \
|
90
|
-
"placed in #{default_loc}, specified with the mauth_config_yml option, or specified with the " \
|
91
|
-
'MAUTH_CONFIG_YML environment variable.'
|
92
|
-
end
|
93
|
-
end
|
46
|
+
# find mauth config
|
47
|
+
mauth_config = options['mauth_config'] || ConfigEnv.load
|
94
48
|
|
95
49
|
unless mauth_config.key?('logger')
|
96
50
|
# the logger. Rails.logger if it exists, otherwise, no logger
|
@@ -106,18 +60,13 @@ module MAuth
|
|
106
60
|
|
107
61
|
# new client with the given App UUID and public key. config may include the following (all
|
108
62
|
# config keys may be strings or symbols):
|
109
|
-
# - private_key - required for signing and for authenticating responses.
|
110
|
-
#
|
111
|
-
# MAuth::Rack::RequestAuthenticator). may be given as a string or a OpenSSL::PKey::RSA
|
112
|
-
# instance.
|
63
|
+
# - private_key - required for signing and for authenticating responses.
|
64
|
+
# may be given as a string or a OpenSSL::PKey::RSA instance.
|
113
65
|
# - app_uuid - required in the same circumstances where a private_key is required
|
114
|
-
# - mauth_baseurl - required. needed
|
115
|
-
# for remote authentication for hopefully obvious reasons.
|
66
|
+
# - mauth_baseurl - required. needed to retrieve public keys.
|
116
67
|
# - mauth_api_version - required. only 'v1' exists / is supported as of this writing.
|
117
68
|
# - logger - a Logger to which any useful information will be written. if this is omitted and
|
118
69
|
# Rails.logger exists, that will be used.
|
119
|
-
# - authenticator - this pretty much never needs to be specified. LocalAuthenticator or
|
120
|
-
# RemoteRequestAuthenticator will be used as appropriate.
|
121
70
|
def initialize(config = {})
|
122
71
|
# stringify symbol keys
|
123
72
|
given_config = config.stringify_symbol_keys
|
@@ -159,25 +108,13 @@ module MAuth
|
|
159
108
|
@config['ssl_certs_path'] = given_config['ssl_certs_path'] if given_config['ssl_certs_path']
|
160
109
|
@config['v2_only_authenticate'] = given_config['v2_only_authenticate'].to_s.casecmp('true').zero?
|
161
110
|
@config['v2_only_sign_requests'] = given_config['v2_only_sign_requests'].to_s.casecmp('true').zero?
|
162
|
-
@config['disable_fallback_to_v1_on_v2_failure'] =
|
163
|
-
given_config['disable_fallback_to_v1_on_v2_failure'].to_s.casecmp('true').zero?
|
164
111
|
@config['v1_only_sign_requests'] = given_config['v1_only_sign_requests'].to_s.casecmp('true').zero?
|
165
|
-
|
166
112
|
if @config['v2_only_sign_requests'] && @config['v1_only_sign_requests']
|
167
113
|
raise MAuth::Client::ConfigurationError, 'v2_only_sign_requests and v1_only_sign_requests may not both be true'
|
168
114
|
end
|
169
115
|
|
170
|
-
|
171
|
-
|
172
|
-
@config['authenticator'] = given_config['authenticator']
|
173
|
-
elsif client_app_uuid && private_key
|
174
|
-
@config['authenticator'] = LocalAuthenticator
|
175
|
-
# MAuth::Client can authenticate locally if it's provided a client_app_uuid and private_key
|
176
|
-
else
|
177
|
-
# otherwise, it will authenticate remotely (requests only)
|
178
|
-
@config['authenticator'] = RemoteRequestAuthenticator
|
179
|
-
end
|
180
|
-
extend @config['authenticator'] if @config['authenticator']
|
116
|
+
@config['disable_fallback_to_v1_on_v2_failure'] =
|
117
|
+
given_config['disable_fallback_to_v1_on_v2_failure'].to_s.casecmp('true').zero?
|
181
118
|
end
|
182
119
|
|
183
120
|
def logger
|
@@ -246,27 +183,4 @@ module MAuth
|
|
246
183
|
hash
|
247
184
|
end
|
248
185
|
end
|
249
|
-
|
250
|
-
module ConfigFile
|
251
|
-
GITHUB_URL = 'https://github.com/mdsol/mauth-client-ruby'
|
252
|
-
@config = {}
|
253
|
-
|
254
|
-
def self.load(path)
|
255
|
-
unless File.exist?(path)
|
256
|
-
raise "File #{path} not found. Please visit #{GITHUB_URL} for details."
|
257
|
-
end
|
258
|
-
|
259
|
-
@config[path] ||= yaml_safe_load_file(path)
|
260
|
-
unless @config[path]
|
261
|
-
raise "File #{path} does not contain proper YAML information. Visit #{GITHUB_URL} for details."
|
262
|
-
end
|
263
|
-
|
264
|
-
@config[path]
|
265
|
-
end
|
266
|
-
|
267
|
-
def self.yaml_safe_load_file(path)
|
268
|
-
yml_data = File.read(path)
|
269
|
-
YAML.safe_load(yml_data, aliases: true)
|
270
|
-
end
|
271
|
-
end
|
272
186
|
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MAuth
|
4
|
+
class ConfigEnv
|
5
|
+
GITHUB_URL = 'https://github.com/mdsol/mauth-client-ruby'
|
6
|
+
|
7
|
+
ENV_STUFF = {
|
8
|
+
'MAUTH_URL' => nil,
|
9
|
+
'MAUTH_API_VERSION' => 'v1',
|
10
|
+
'MAUTH_APP_UUID' => nil,
|
11
|
+
'MAUTH_PRIVATE_KEY' => nil,
|
12
|
+
'MAUTH_PRIVATE_KEY_FILE' => 'config/mauth_key',
|
13
|
+
'MAUTH_V2_ONLY_AUTHENTICATE' => false,
|
14
|
+
'MAUTH_V2_ONLY_SIGN_REQUESTS' => false,
|
15
|
+
'MAUTH_DISABLE_FALLBACK_TO_V1_ON_V2_FAILURE' => false,
|
16
|
+
'MAUTH_V1_ONLY_SIGN_REQUESTS' => true
|
17
|
+
}.freeze
|
18
|
+
|
19
|
+
class << self
|
20
|
+
def load
|
21
|
+
validate! if production?
|
22
|
+
|
23
|
+
{
|
24
|
+
'mauth_baseurl' => env[:mauth_url] || 'http://localhost:7000',
|
25
|
+
'mauth_api_version' => env[:mauth_api_version],
|
26
|
+
'app_uuid' => env[:mauth_app_uuid] || 'fb17460e-9868-11e1-8399-0090f5ccb4d3',
|
27
|
+
'private_key' => private_key || generate_private_key,
|
28
|
+
'v2_only_authenticate' => env[:mauth_v2_only_authenticate],
|
29
|
+
'v2_only_sign_requests' => env[:mauth_v2_only_sign_requests],
|
30
|
+
'disable_fallback_to_v1_on_v2_failure' => env[:mauth_disable_fallback_to_v1_on_v2_failure],
|
31
|
+
'v1_only_sign_requests' => env[:mauth_v1_only_sign_requests]
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def validate!
|
38
|
+
errors = []
|
39
|
+
errors << 'The MAUTH_URL environment variable must be set' if env[:mauth_url].nil?
|
40
|
+
errors << 'The MAUTH_APP_UUID environment variable must be set' if env[:mauth_app_uuid].nil?
|
41
|
+
errors << 'The MAUTH_PRIVATE_KEY environment variable must be set' if env[:mauth_private_key].nil?
|
42
|
+
return if errors.empty?
|
43
|
+
|
44
|
+
errors.map! { |err| "#{err} => See #{GITHUB_URL}" }
|
45
|
+
errors.unshift('Invalid MAuth Client configuration:')
|
46
|
+
raise errors.join("\n")
|
47
|
+
end
|
48
|
+
|
49
|
+
def env
|
50
|
+
@env ||= ENV_STUFF.each_with_object({}) do |(key, default), hsh|
|
51
|
+
env_key = key.downcase.to_sym
|
52
|
+
hsh[env_key] = ENV.fetch(key, default)
|
53
|
+
|
54
|
+
case default
|
55
|
+
when TrueClass, FalseClass
|
56
|
+
hsh[env_key] = hsh[env_key].to_s.casecmp('true').zero?
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def production?
|
62
|
+
environment.to_s.casecmp('production').zero?
|
63
|
+
end
|
64
|
+
|
65
|
+
def environment
|
66
|
+
return Rails.environment if Object.const_defined?(:Rails) && ::Rails.respond_to?(:environment)
|
67
|
+
|
68
|
+
ENV.fetch('RAILS_ENV') { ENV.fetch('RACK_ENV', 'development') }
|
69
|
+
end
|
70
|
+
|
71
|
+
def private_key
|
72
|
+
return env[:mauth_private_key] if env[:mauth_private_key]
|
73
|
+
return nil unless env[:mauth_private_key_file] && File.readable?(env[:mauth_private_key_file])
|
74
|
+
|
75
|
+
File.read(env[:mauth_private_key_file])
|
76
|
+
end
|
77
|
+
|
78
|
+
def generate_private_key
|
79
|
+
require 'openssl'
|
80
|
+
OpenSSL::PKey::RSA.generate(2048).to_s
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
data/lib/mauth/version.rb
CHANGED
data/mauth-client.gemspec
CHANGED
@@ -14,7 +14,7 @@ Gem::Specification.new do |spec|
|
|
14
14
|
'Includes middleware for Rack and Faraday for incoming and outgoing requests and responses.'
|
15
15
|
spec.homepage = 'https://github.com/mdsol/mauth-client-ruby'
|
16
16
|
spec.license = 'MIT'
|
17
|
-
spec.required_ruby_version = '>= 2.
|
17
|
+
spec.required_ruby_version = '>= 2.7.0'
|
18
18
|
|
19
19
|
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
20
20
|
spec.bindir = 'exe'
|
@@ -23,23 +23,8 @@ Gem::Specification.new do |spec|
|
|
23
23
|
|
24
24
|
spec.add_dependency 'addressable', '~> 2.0'
|
25
25
|
spec.add_dependency 'coderay', '~> 1.0'
|
26
|
-
spec.add_dependency 'dice_bag', '>= 0.9', '< 2.0'
|
27
26
|
spec.add_dependency 'faraday', '>= 0.9', '< 3.0'
|
28
27
|
spec.add_dependency 'faraday-http-cache', '>= 2.0', '< 3.0'
|
29
|
-
spec.add_dependency 'rack'
|
28
|
+
spec.add_dependency 'rack', '> 2.2.3'
|
30
29
|
spec.add_dependency 'term-ansicolor', '~> 1.0'
|
31
|
-
|
32
|
-
spec.add_development_dependency 'appraisal'
|
33
|
-
spec.add_development_dependency 'benchmark-ips', '~> 2.7'
|
34
|
-
spec.add_development_dependency 'bundler', '>= 1.17'
|
35
|
-
spec.add_development_dependency 'byebug'
|
36
|
-
spec.add_development_dependency 'rack-test', '~> 1.1.0'
|
37
|
-
spec.add_development_dependency 'rake', '~> 12.0'
|
38
|
-
spec.add_development_dependency 'rspec', '~> 3.8'
|
39
|
-
spec.add_development_dependency 'rubocop', '= 1.25.1'
|
40
|
-
spec.add_development_dependency 'rubocop-mdsol', '~> 0.1'
|
41
|
-
spec.add_development_dependency 'rubocop-performance', '= 1.13.2'
|
42
|
-
spec.add_development_dependency 'simplecov', '~> 0.16'
|
43
|
-
spec.add_development_dependency 'timecop', '~> 0.9'
|
44
|
-
spec.add_development_dependency 'webmock', '~> 3.0'
|
45
30
|
end
|