keycloak_rack 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/main.yml +68 -0
  3. data/.gitignore +8 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +220 -0
  6. data/.ruby-version +1 -0
  7. data/.yardopts +7 -0
  8. data/Appraisals +16 -0
  9. data/CHANGELOG.md +10 -0
  10. data/CODE_OF_CONDUCT.md +132 -0
  11. data/Gemfile +5 -0
  12. data/LICENSE +19 -0
  13. data/README.md +288 -0
  14. data/Rakefile +10 -0
  15. data/bin/appraisal +29 -0
  16. data/bin/console +6 -0
  17. data/bin/fix-appraisals +14 -0
  18. data/bin/rake +29 -0
  19. data/bin/rspec +29 -0
  20. data/bin/rubocop +29 -0
  21. data/bin/yard +29 -0
  22. data/bin/yardoc +29 -0
  23. data/bin/yri +29 -0
  24. data/gemfiles/rack_only.gemfile +5 -0
  25. data/gemfiles/rack_only.gemfile.lock +204 -0
  26. data/gemfiles/rails_6_0.gemfile +9 -0
  27. data/gemfiles/rails_6_0.gemfile.lock +323 -0
  28. data/gemfiles/rails_6_1.gemfile +9 -0
  29. data/gemfiles/rails_6_1.gemfile.lock +326 -0
  30. data/keycloak_rack.gemspec +56 -0
  31. data/lib/keycloak_rack.rb +59 -0
  32. data/lib/keycloak_rack/authenticate.rb +115 -0
  33. data/lib/keycloak_rack/authorize_realm.rb +53 -0
  34. data/lib/keycloak_rack/authorize_resource.rb +54 -0
  35. data/lib/keycloak_rack/config.rb +84 -0
  36. data/lib/keycloak_rack/container.rb +53 -0
  37. data/lib/keycloak_rack/decoded_token.rb +191 -0
  38. data/lib/keycloak_rack/flexible_struct.rb +20 -0
  39. data/lib/keycloak_rack/http_client.rb +86 -0
  40. data/lib/keycloak_rack/import.rb +9 -0
  41. data/lib/keycloak_rack/key_fetcher.rb +20 -0
  42. data/lib/keycloak_rack/key_resolver.rb +64 -0
  43. data/lib/keycloak_rack/middleware.rb +132 -0
  44. data/lib/keycloak_rack/railtie.rb +14 -0
  45. data/lib/keycloak_rack/read_token.rb +40 -0
  46. data/lib/keycloak_rack/resource_role_map.rb +8 -0
  47. data/lib/keycloak_rack/role_map.rb +15 -0
  48. data/lib/keycloak_rack/session.rb +44 -0
  49. data/lib/keycloak_rack/skip_authentication.rb +44 -0
  50. data/lib/keycloak_rack/types.rb +42 -0
  51. data/lib/keycloak_rack/version.rb +6 -0
  52. data/lib/keycloak_rack/with_config.rb +15 -0
  53. data/spec/dummy/.ruby-version +1 -0
  54. data/spec/dummy/README.md +24 -0
  55. data/spec/dummy/Rakefile +8 -0
  56. data/spec/dummy/app/controllers/application_controller.rb +22 -0
  57. data/spec/dummy/app/controllers/test_controller.rb +9 -0
  58. data/spec/dummy/config.ru +8 -0
  59. data/spec/dummy/config/application.rb +52 -0
  60. data/spec/dummy/config/boot.rb +3 -0
  61. data/spec/dummy/config/environment.rb +7 -0
  62. data/spec/dummy/config/environments/development.rb +51 -0
  63. data/spec/dummy/config/environments/test.rb +51 -0
  64. data/spec/dummy/config/initializers/application_controller_renderer.rb +9 -0
  65. data/spec/dummy/config/initializers/backtrace_silencers.rb +10 -0
  66. data/spec/dummy/config/initializers/cors.rb +17 -0
  67. data/spec/dummy/config/initializers/filter_parameter_logging.rb +8 -0
  68. data/spec/dummy/config/initializers/inflections.rb +17 -0
  69. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  70. data/spec/dummy/config/initializers/wrap_parameters.rb +11 -0
  71. data/spec/dummy/config/keycloak.yml +12 -0
  72. data/spec/dummy/config/locales/en.yml +33 -0
  73. data/spec/dummy/config/routes.rb +5 -0
  74. data/spec/dummy/public/robots.txt +1 -0
  75. data/spec/dummy/tmp/development_secret.txt +1 -0
  76. data/spec/factories/decoded_token.rb +18 -0
  77. data/spec/factories/session.rb +21 -0
  78. data/spec/factories/token_payload.rb +40 -0
  79. data/spec/keycloak_rack/authorize_realm_spec.rb +15 -0
  80. data/spec/keycloak_rack/authorize_resource_spec.rb +19 -0
  81. data/spec/keycloak_rack/decoded_token_spec.rb +31 -0
  82. data/spec/keycloak_rack/key_resolver_spec.rb +95 -0
  83. data/spec/keycloak_rack/middleware_spec.rb +172 -0
  84. data/spec/keycloak_rack/rails_integration_spec.rb +43 -0
  85. data/spec/keycloak_rack/session_spec.rb +37 -0
  86. data/spec/keycloak_rack/skip_authentication_spec.rb +55 -0
  87. data/spec/spec_helper.rb +101 -0
  88. data/spec/support/contexts/mocked_keycloak.rb +63 -0
  89. data/spec/support/contexts/mocked_rack_application.rb +41 -0
  90. data/spec/support/test_key.pem +27 -0
  91. data/spec/support/token_helper.rb +76 -0
  92. metadata +616 -0
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright 2021 Alexa Grey
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the "Software"), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7
+ of the Software, and to permit persons to whom the Software is furnished to do
8
+ so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,288 @@
1
+ # keycloak_rack
2
+
3
+ An opinionated, convention-over-configuration gem to authenticate Rack (and Rails) applications
4
+ against a [Keycloak](https://www.keycloak.org/) installation. It uses a lot of features from the
5
+ [dry-rb ecosystem](https://dry-rb.org), and works well in applications that do the same.
6
+
7
+ In particular, it adopts a [monadic approach](https://dry-rb.org/gems/dry-monads/1.3/) to authentication flow control,
8
+ allowing for more granularity in how the whole process is handled.
9
+
10
+ ## Install
11
+
12
+ ```ruby
13
+ gem "keycloak_rack", "1.0.0"
14
+ ```
15
+
16
+ ### Ruby & Rails Versions
17
+
18
+ - Ruby 2.7, 3.0
19
+ - Rails 6.0, 6.1, or using only Rack 2.2
20
+
21
+ It has also been tested on Rails 5.2, but isn't officially supported because it doesn't support Ruby 3.
22
+
23
+ At minimum, it requires Ruby 2.7, because it makes use of [pattern matching](https://docs.ruby-lang.org/en/3.0.0/doc/syntax/pattern_matching_rdoc.html).
24
+ If you find the warning at boot annoying (I sure do), you can set `RUBYOPT='-W:no-experimental'` in your environment to silence the nag.
25
+
26
+ ## Basic Usage in Rails
27
+
28
+ `KeycloakRack` attaches itself as Rack middleware and processes the `Authorization` header passed to the application (if any).
29
+
30
+ Once it runs, it attaches itself to the rack environment in a number of places,
31
+ but the primary entry point is `keycloak:session`:
32
+
33
+ ```ruby
34
+ class ApplicationController < ActionController::API
35
+ before_action :authenticate_user!
36
+
37
+ # @return [void]
38
+ def authenticate_user!
39
+ # KeycloakRack::Session#authenticate! implements a Dry::Matcher::ResultMatcher
40
+ request.env["keycloak:session"].authenticate! do |m|
41
+ m.success(:authenticated) do |_, token|
42
+ # this is the case when a user is successfully authenticated
43
+
44
+ # token will be a KeycloakRack::DecodedToken instance, a
45
+ # hash-like PORO that maps a number of values from the
46
+ # decoded JWT that can be used to find or upsert a user
47
+
48
+ attrs = decoded_token.slice(:keycloak_id, :email, :email_verified, :realm_access, :resource_access)
49
+
50
+ result = User.upsert attrs, returning: %i[id], unique_by: %i[keycloak_id]
51
+
52
+ @current_user = User.find result.first["id"]
53
+ end
54
+
55
+ m.success do
56
+ # When allow_anonymous is true, or
57
+ # a URI is skipped because of skip_paths, this
58
+ # case will be reached. Requests from here on
59
+ # out should be considered anonymous and treated
60
+ # accordingly
61
+
62
+ @current_user = AnonymousUser.new
63
+ end
64
+
65
+ m.failure do |code, reason|
66
+ # All authentication failures are reached here,
67
+ # assuming halt_on_auth_failure is set to false
68
+ # This allows the application to decide how it
69
+ # wants to respond
70
+
71
+ render json: { errors: [{ message: "Auth Failure" }] }, status: :forbidden
72
+ end
73
+ end
74
+ end
75
+ end
76
+ ```
77
+
78
+ ## Configuration
79
+
80
+ This gem uses [anyway_config](https://github.com/palkan/anyway_config), which allows you to make use of ENV vars, Rails credentials,
81
+ and simple YAML configuration files interchangeably.
82
+
83
+ At minimum, you must configure `server_url` and `realm_id` to authenticate a user's token against your Keycloak instance.
84
+
85
+ | Option | ENV | Default Value | Type | Required? | Description | Example |
86
+ | ---- | ----- | ----- | ------ | ----- | ------ | ----- |
87
+ | `server_url` | `KEYCLOAK_SERVER_URL` | `nil` | `String` | Required | The base url where your Keycloak server is located. This value can be retrieved in your Keycloak client configuration. | `auth:8080` |
88
+ | `realm_id` | `KEYCLOAK_REALM_ID` | `nil` | `String` | Required | Realm's name (not id, actually) | `master` |
89
+ | `token_leeway` | `KEYCLOAK_TOKEN_LEEWAY` | `10` | `Integer` | Optional | Number of seconds a token can expire before being rejected by the API. | `15` | 
90
+ | `allow_anonymous` | `KEYCLOAK_ALLOW_ANONYMOUS` | `false` | `Boolean` | Optional | Whether to allow anonymous users to access the API. If true, authentication will not provided a decoded token instance | `true` |
91
+ | `halt_on_auth_failure` | `KEYCLOAK_HALT_ON_AUTH_FAILURE` | `true` | `Boolean` | Optional | Whether to short-circuit when a token is invalid, or otherwise fails (if `allow_anonymous` is false, token-less access counts as a failure). Set this to `false` if you want to handle failures in your application instead. | `false` |
92
+ | `cache_ttl` | `KEYCLOAK_CACHE_TTL` | `86400` | `Integer` | Optional | Interval (in seconds) to cache public keys from Keycloak. These should not change very often, so 1 day (86400) is the default. | `86400` | 
93
+ | `ca_certificate_file` | `KEYCLOAK_CA_CERTIFICATE_FILE` | `nil` | `String` | Optional | Path to the certificate authority used to validate the Keycloak server certificate | `/credentials/production_root_ca_cert.pem` | 
94
+ | `skip_paths` | _n/a_ | `{}` | `Hash` | Optional | Paths where token validation is skipped | `{ get: %w[/ping], post: [%r,/stats,] }`| 
95
+
96
+ ### Options
97
+
98
+ Because of `anyway_config`, you can create a file `config/keycloak.yml` to populate most of the settings.
99
+
100
+ ```yml
101
+ default: &default
102
+ server_url: "https://keycloak.example.com/auth"
103
+ realm_id: Test
104
+
105
+ development:
106
+ <<: *default
107
+
108
+ test:
109
+ <<: *default
110
+
111
+ production:
112
+ <<: *default
113
+ ```
114
+
115
+ [Rails credentials](https://guides.rubyonrails.org/security.html#custom-credentials) under the key `keycloak` will also work:
116
+
117
+ ```yml
118
+ keycloak:
119
+ server_url: "https://keycloak.example.com/auth"
120
+ realm_id: "Test"
121
+ ```
122
+
123
+ You can also do a more traditional approach in an initializer, but note that any changes here
124
+ will _override_ values inherited by anyway_config's approach. It's really only useful for
125
+ configuring `skip_paths`, given its support for regular expressions.
126
+
127
+ ```ruby
128
+ KeycloakRack.configure do |config|
129
+ config.server_url = ENV["KEYCLOAK_SERVER_URL"]
130
+ config.realm_id = ENV["KEYCLOAK_REALM_ID"]
131
+ config.skip_paths = {
132
+ get: ["/ping"],
133
+ post: [%r,/api/v1/analytics,]
134
+ }
135
+ end
136
+ ```
137
+
138
+ ## Usage
139
+
140
+ ### Authorizing a realm role
141
+
142
+ There is a helper service that gets mounted in the Rack environment as `keycloak:authorize_realm`, and works
143
+ similarly to the session's authenticate method:
144
+
145
+ ```ruby
146
+ class UploadProcessor
147
+ def initialize(app)
148
+ @app = app
149
+ end
150
+
151
+ def call(env)
152
+ env["keycloak.authorize_realm"].call("upload_permission") do |m|
153
+ m.success do
154
+ # allow the upload to proceed
155
+ end
156
+
157
+ m.failure do
158
+ # fail the response, return 403, etc
159
+ end
160
+ end
161
+ end
162
+ end
163
+
164
+ app = Rack::Builder.app do
165
+ use KeycloakRack::Middleware
166
+
167
+ run UploadProcessor
168
+ end
169
+ ```
170
+
171
+ ### Authorizing a resource role
172
+
173
+ There is also a helper service that gets mounted as `keycloak:authorize_resource`,
174
+ for checking resource roles:
175
+
176
+ ```ruby
177
+ class WidgetCombobulator
178
+ def initialize(app)
179
+ @app = app
180
+ end
181
+
182
+ def call(env)
183
+ env["keycloak.authorize_resource"].call("widgets", "recombobulate") do |m|
184
+ m.success do
185
+ # allow the user to recombobulate the widget
186
+ end
187
+
188
+ m.failure do
189
+ # return forbidden, log the attempt, etc
190
+ end
191
+ end
192
+ end
193
+ end
194
+
195
+ app = Rack::Builder.app do
196
+ use KeycloakRack::Middleware
197
+
198
+ run WidgetCombobulator
199
+ end
200
+ ```
201
+
202
+ ### Overriding the failure response
203
+
204
+ The easiest approach would be to set `halt_on_auth_failure` to `false` and handle the failure in your application,
205
+ but the middleware has a few spots that can be hooked into with a prepended module if you'd prefer to monkey patch.
206
+
207
+ ```ruby
208
+ module Patches
209
+ module OverrideKeycloakFailureBody
210
+ # @param [Hash] env
211
+ # @param [Dry::Monads::Failure] monad
212
+ # @return [String, #to_json]
213
+ def build_failure_body(env, monad)
214
+ # You can use the #failure method on the monad to retrieve a tuple
215
+ reason, message, token, original_error = monad.failure
216
+
217
+ # reason is a symbol, like :no_token or :expired
218
+ # message is a human-readable string that explains why it failed
219
+ # token is the original token (if any) that was provided
220
+ # original_error is a possible exception that was raised (not all failures have one)
221
+
222
+ # Return any object that will JSONify itself with #to_json
223
+
224
+ {
225
+ error: "You can't sign in because: #{message}"
226
+ }
227
+ end
228
+ end
229
+ end
230
+
231
+ KeycloakRack::Middleware.prepend Patches::OverrideKeycloakFailureBody
232
+ ```
233
+
234
+ If you need to return something other than JSON,
235
+ or otherwise augment the headers, you can do something like:
236
+
237
+ ```ruby
238
+ module Patches
239
+ module OverrideKeycloakFailureHeaders
240
+ # @param [Hash] env
241
+ # @param [Dry::Monads::Failure] monad
242
+ # @return [{ String => String }]
243
+ def build_failure_headers(env, monad)
244
+ {
245
+ "Content-Type" => "application/xml",
246
+ "Special-Header" => "special-value",
247
+ }
248
+ end
249
+ end
250
+ end
251
+
252
+ KeycloakRack::Middleware.prepend Patches::OverrideKeycloakFailureHeaders
253
+ ```
254
+
255
+ In the future, this might be customizable, but it's low priority.
256
+
257
+ ## History
258
+
259
+ What became this gem started out as a slight modification to [keycloak-api-rails](https://github.com/looorent/keycloak-api-rails)
260
+ by [looorent](https://github.com/looorent). For authenticating requests a Rails API that must _always_ have a token,
261
+ that gem works great and I would recommend it.
262
+
263
+ As I continued building my application, I had some needs that weren't met by it, namely:
264
+
265
+ - Anonymous user access—I just need to know if the user is authenticated or not without preventing
266
+ access to the application, I'll handle failures myself.
267
+ - Control over auth failures in general (this is still pending, though made easier to monkey-patch)
268
+ - Usage outside of Rails—I have some microservices that are rack applications.
269
+ - Easier role checking for rack middleware.
270
+ - Stricter auth: no query strings. I want my APIs to only support clients that send an `Authorization`
271
+ header with a bearer token.
272
+
273
+ I ended up rewriting it from scratch, but the logic in this owes a lot to the original author's design.
274
+
275
+ ## Future extensions
276
+
277
+ - A way to extract custom attributes from the token besides the defaults Keycloak provides,
278
+ presently there's no way to get at those.
279
+
280
+ ## Contributing
281
+
282
+ Bug reports and pull requests are welcome on GitHub at https://github.com/scryptmouse/keycloak_rack.
283
+ This project is intended to be a safe, welcoming space for collaboration, and contributors are
284
+ expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
285
+
286
+ ## License
287
+
288
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec) do |c|
7
+ c.verbose = false
8
+ end
9
+
10
+ task default: :spec
data/bin/appraisal ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'appraisal' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("appraisal", "appraisal")
data/bin/console ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative "../lib/keycloak_rack.rb"
4
+ require "pry"
5
+
6
+ Pry.start
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env bash
2
+
3
+ root="$(realpath "$(dirname $0)/..")"
4
+
5
+ echo $root
6
+
7
+ for gemfile in $(find "$root/gemfiles" -iname "*.gemfile"); do
8
+ lockfile="${gemfile}.lock"
9
+
10
+ echo "Fixing $gemfile..." >&2
11
+
12
+ BUNDLE_GEMFILE="${gemfile}" bundle lock --lockfile="${lockfile}" --add-platform x86_64-linux > /dev/null
13
+ BUNDLE_GEMFILE="${gemfile}" bundle lock --lockfile="${lockfile}" --add-platform ruby > /dev/null
14
+ done
data/bin/rake ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rake' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rake", "rake")
data/bin/rspec ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rspec' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rspec-core", "rspec")
data/bin/rubocop ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rubocop' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rubocop", "rubocop")