keycloak_rack 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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")