stitches 3.8.0 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f22af42820e6175c739a77f2f6945dd3d6aea8f94b43509303ed27874e315ca4
4
- data.tar.gz: fee8fce193a17741483531c884cd20638ad92e1b5ce423a0673d85ee467aa5f3
3
+ metadata.gz: 3ea8633778064ab7646860c9a0682a7290bfbea3d6f3f091c290f50d22f97de2
4
+ data.tar.gz: 0eb1d04f15fc37ab0b0484ae6a73f953426cc5dd1b9efd056e42300caa4c2d04
5
5
  SHA512:
6
- metadata.gz: 24c0456e68046e5a7a3d9cd045f35fe3095659a25da0e77b5d623ff64c759893a5c46a9367c898d58287312a29b85a186613cdaeda09558036ffa698d2e68fd1
7
- data.tar.gz: e019d85dd736bf28b5e751ffef132cae093019877f6c5aa743e0a39f4b51e1d7da37051aa8ee9ad799db13d27fd24d8cfb1df7b8472ba2491c198ac8cdc0f27c
6
+ metadata.gz: f854d4c3feca2e48b6edfde3174564112b2c0c1d6cb6b93bed960985cc779c1119bcf5ddb65d43d85d3c349d9f440b61e14b1cef27aa6f8c1701768ab5ac1dd5
7
+ data.tar.gz: b19d1b967d4a5e0dc57265475683bbfd8e9f255d15ee0af1a8130832d16e73e54b52b7e34fc025fe170a4322cb30cbddb6f2e631f7c81521953e4e48514bb547
@@ -5,7 +5,7 @@ version: 2
5
5
  jobs:
6
6
  release:
7
7
  docker:
8
- - image: circleci/ruby:2.6.3
8
+ - image: circleci/ruby:2.7.1
9
9
  steps:
10
10
  - checkout
11
11
  - run: bundle config stitchfix01.jfrog.io $ARTIFACTORY_USER:$ARTIFACTORY_TOKEN
@@ -17,11 +17,11 @@ jobs:
17
17
  - run:
18
18
  name: Build/release gem to artifactory
19
19
  command: bundle exec rake push_artifactory
20
- ruby-2.6.3-rails-5.2:
20
+ ruby-2.7.1-rails-6.0:
21
21
  docker:
22
- - image: circleci/ruby:2.6.3
22
+ - image: circleci/ruby:2.7.1
23
23
  environment:
24
- BUNDLE_GEMFILE: Gemfile.rails-5.2
24
+ BUNDLE_GEMFILE: Gemfile.rails-6.0
25
25
  working_directory: "~/stitches"
26
26
  steps:
27
27
  - checkout
@@ -35,15 +35,15 @@ jobs:
35
35
  fi
36
36
  - run:
37
37
  name: Notify Pager Duty
38
- command: bundle exec y-notify "#eng-platform"
38
+ command: bundle exec y-notify "#devex-alerts"
39
39
  when: on_fail
40
40
  - store_test_results:
41
41
  path: "/tmp/test-results"
42
- ruby-2.5.5-rails-5.2:
42
+ ruby-2.6.6-rails-6.0:
43
43
  docker:
44
- - image: circleci/ruby:2.5.5
44
+ - image: circleci/ruby:2.6.6
45
45
  environment:
46
- BUNDLE_GEMFILE: Gemfile.rails-5.2
46
+ BUNDLE_GEMFILE: Gemfile.rails-6.0
47
47
  working_directory: "~/stitches"
48
48
  steps:
49
49
  - checkout
@@ -57,15 +57,15 @@ jobs:
57
57
  fi
58
58
  - run:
59
59
  name: Notify Pager Duty
60
- command: bundle exec y-notify "#eng-platform"
60
+ command: bundle exec y-notify "#devex-alerts"
61
61
  when: on_fail
62
62
  - store_test_results:
63
63
  path: "/tmp/test-results"
64
- ruby-2.6.3-rails-5.1:
64
+ ruby-2.7.1-rails-5.2:
65
65
  docker:
66
- - image: circleci/ruby:2.6.3
66
+ - image: circleci/ruby:2.7.1
67
67
  environment:
68
- BUNDLE_GEMFILE: Gemfile.rails-5.1
68
+ BUNDLE_GEMFILE: Gemfile.rails-5.2
69
69
  working_directory: "~/stitches"
70
70
  steps:
71
71
  - checkout
@@ -79,15 +79,15 @@ jobs:
79
79
  fi
80
80
  - run:
81
81
  name: Notify Pager Duty
82
- command: bundle exec y-notify "#eng-platform"
82
+ command: bundle exec y-notify "#devex-alerts"
83
83
  when: on_fail
84
84
  - store_test_results:
85
85
  path: "/tmp/test-results"
86
- ruby-2.5.5-rails-5.1:
86
+ ruby-2.6.6-rails-5.2:
87
87
  docker:
88
- - image: circleci/ruby:2.5.5
88
+ - image: circleci/ruby:2.6.6
89
89
  environment:
90
- BUNDLE_GEMFILE: Gemfile.rails-5.1
90
+ BUNDLE_GEMFILE: Gemfile.rails-5.2
91
91
  working_directory: "~/stitches"
92
92
  steps:
93
93
  - checkout
@@ -101,7 +101,7 @@ jobs:
101
101
  fi
102
102
  - run:
103
103
  name: Notify Pager Duty
104
- command: bundle exec y-notify "#eng-platform"
104
+ command: bundle exec y-notify "#devex-alerts"
105
105
  when: on_fail
106
106
  - store_test_results:
107
107
  path: "/tmp/test-results"
@@ -112,31 +112,31 @@ workflows:
112
112
  - release:
113
113
  context: org-global
114
114
  requires:
115
- - ruby-2.6.3-rails-5.2
116
- - ruby-2.5.5-rails-5.2
117
- - ruby-2.6.3-rails-5.1
118
- - ruby-2.5.5-rails-5.1
115
+ - ruby-2.7.1-rails-6.0
116
+ - ruby-2.6.6-rails-6.0
117
+ - ruby-2.7.1-rails-5.2
118
+ - ruby-2.6.6-rails-5.2
119
119
  filters:
120
120
  tags:
121
- only: /^[0-9]+\.[0-9]+\.[0-9](\.RC\d*)?$/
121
+ only: /^[0-9]+\.[0-9]+\.[0-9]+(\.?(RC|rc)[-\.]?\d*)?$/
122
122
  branches:
123
123
  ignore: /.*/
124
- - ruby-2.6.3-rails-5.2:
124
+ - ruby-2.7.1-rails-6.0:
125
125
  context: org-global
126
126
  filters:
127
127
  tags:
128
128
  only: &1 /.*/
129
- - ruby-2.5.5-rails-5.2:
129
+ - ruby-2.6.6-rails-6.0:
130
130
  context: org-global
131
131
  filters:
132
132
  tags:
133
133
  only: *1
134
- - ruby-2.6.3-rails-5.1:
134
+ - ruby-2.7.1-rails-5.2:
135
135
  context: org-global
136
136
  filters:
137
137
  tags:
138
138
  only: *1
139
- - ruby-2.5.5-rails-5.1:
139
+ - ruby-2.6.6-rails-5.2:
140
140
  context: org-global
141
141
  filters:
142
142
  tags:
@@ -150,11 +150,11 @@ workflows:
150
150
  only:
151
151
  - master
152
152
  jobs:
153
- - ruby-2.6.3-rails-5.2:
153
+ - ruby-2.7.1-rails-6.0:
154
154
  context: org-global
155
- - ruby-2.5.5-rails-5.2:
155
+ - ruby-2.6.6-rails-6.0:
156
156
  context: org-global
157
- - ruby-2.6.3-rails-5.1:
157
+ - ruby-2.7.1-rails-5.2:
158
158
  context: org-global
159
- - ruby-2.5.5-rails-5.1:
159
+ - ruby-2.6.6-rails-5.2:
160
160
  context: org-global
@@ -8,4 +8,4 @@
8
8
  # This file uses the GitHub CODEOWNERS convention to assign PR reviewers:
9
9
  # https://help.github.com/articles/about-codeowners/
10
10
 
11
- * @brettfishman @bwebster
11
+ * @brettfishman @bwebster @stitchfix/devex
@@ -1 +1 @@
1
- 2.6.3
1
+ 2.7.1
@@ -1,7 +1,10 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 2.4.4
4
- - 2.5.1
3
+ - 2.6
4
+ - 2.7
5
5
  - ruby-head
6
6
  notifications:
7
7
  email: false
8
+ jobs:
9
+ allow_failures:
10
+ - rvm: ruby-head
@@ -1,7 +1,6 @@
1
1
  # DO NOT MODIFY - this is managed by Git Reduce in goro
2
2
  #
3
- source 'https://gem.fury.io/me/'
4
- source 'https://www.rubygems.org'
3
+ source 'https://stitchfix01.jfrog.io/stitchfix01/api/gems/eng-gems/'
5
4
 
6
5
  gemspec
7
6
 
@@ -1,7 +1,6 @@
1
1
  # DO NOT MODIFY - this is managed by Git Reduce in goro
2
2
  #
3
- source 'https://gem.fury.io/me/'
4
- source 'https://www.rubygems.org'
3
+ source 'https://stitchfix01.jfrog.io/stitchfix01/api/gems/eng-gems/'
5
4
 
6
5
  gemspec
7
6
 
@@ -0,0 +1,7 @@
1
+ # DO NOT MODIFY - this is managed by Git Reduce in goro
2
+ #
3
+ source 'https://stitchfix01.jfrog.io/stitchfix01/api/gems/eng-gems/'
4
+
5
+ gemspec
6
+
7
+ gem 'rails', '~> 6.0.0'
data/README.md CHANGED
@@ -4,13 +4,13 @@ Create Microservices in Rails by pretty much just writing regular Rails code.
4
4
 
5
5
  This gem provides:
6
6
 
7
- * transparent API key authentication.
8
- * router-level API version based on headers.
9
- * a way to document your microservice endpoints via acceptance tests.
10
- * structured errors, buildable from invalid Active Records, Exceptions, or by hand.
7
+ - transparent API key authentication.
8
+ - router-level API version based on headers.
9
+ - a way to document your microservice endpoints via acceptance tests.
10
+ - structured errors, buildable from invalid Active Records, Exceptions, or by hand.
11
11
 
12
12
  This, plus much of what you get from Rails already, means you can create a microservice Rails application by just writing the
13
- same Rails code you write today. Instead of rendering web views, you render JSON (which is built into Rails).
13
+ same Rails code you write today. Instead of rendering web views, you render JSON (which is built into Rails).
14
14
 
15
15
  ## To install
16
16
 
@@ -35,14 +35,26 @@ Then, set it up:
35
35
 
36
36
  ### Upgrading from an older version
37
37
 
38
- * If you have a version lower than 3.3.0, you need to run two generators, one of which creates a new database migration on your
39
- `api_clients` table:
38
+ - When upgrading to version 4.0.0 you may now take advantage of an in-memory cache
39
+
40
+ You can enabled it like so
41
+
42
+ ```ruby
43
+ Stitches.configure do |config|
44
+ config.max_cache_ttl = 5 # seconds
45
+ config.max_cache_size = 100 # how many keys to cache
46
+ end
47
+ ```
48
+
49
+ - If you have a version lower than 3.3.0, you need to run two generators, one of which creates a new database migration on your
50
+ `api_clients` table:
40
51
 
41
52
  ```
42
53
  > bin/rails generate stitches:add_enabled_to_api_clients
43
54
  > bin/rails generate stitches:add_deprecation
44
55
  ```
45
- * If you have a version lower than 3.6.0, you need to run one generator:
56
+
57
+ - If you have a version lower than 3.6.0, you need to run one generator:
46
58
 
47
59
  ```
48
60
  > bin/rails generate stitches:add_deprecation
@@ -59,8 +71,8 @@ class Api::V1::WidgetsController < ApiController
59
71
  if widget.valid?
60
72
  head 201
61
73
  else
62
- render json: {
63
- errors: Stitches::Errors.from_active_record_object(widget)
74
+ render json: {
75
+ errors: Stitches::Errors.from_active_record_object(widget)
64
76
  }, status: 422
65
77
  end
66
78
  end
@@ -73,44 +85,62 @@ private
73
85
  end
74
86
  ```
75
87
 
76
- If you think there's nothing special about this—you are correct. This is the vanillaest of vanilla Rails controllers, with a few
88
+ If you think there's nothing special about this—you are correct. This is the vanillaest of vanilla Rails controllers, with a few
77
89
  notable exceptions:
78
90
 
79
- * We aren't checking content type. A stitches-based microservice always uses JSON and refuses to route requests for non-JSON to
80
- you, so there's zero need to use `respond_to` and friends.
81
- * The error-building is structured and reliable.
82
- * This is an authenticated request. No request without proper authentication will be routed here, so you don't have to worry
83
- about it in your code.
84
- * This is a versioned request. While the URL will *not* contain `v1` in it, the `Accept` header will require a version and get
85
- routed here. If you make a V2, it's just a new controller and this concern is handled at the routing layer.
91
+ - We aren't checking content type. A stitches-based microservice always uses JSON and refuses to route requests for non-JSON to
92
+ you, so there's zero need to use `respond_to` and friends.
93
+ - The error-building is structured and reliable.
94
+ - This is an authenticated request. No request without proper authentication will be routed here, so you don't have to worry
95
+ about it in your code.
96
+ - This is a versioned request. While the URL will _not_ contain `v1` in it, the `Accept` header will require a version and get
97
+ routed here. If you make a V2, it's just a new controller and this concern is handled at the routing layer.
86
98
 
87
- All this means that the Rails skills of you and your team can be directly applied to building microservices. You don't have to make a bunch of boring decisions about auth, versioning, or content-types. It also means you can start deploying and creating microservices with little friction. No need to deal with a complex DSL or new programming language to get yourselves going with Microservices.
99
+ All this means that the Rails skills of you and your team can be directly applied to building microservices. You don't have to make a bunch of boring decisions about auth, versioning, or content-types. It also means you can start deploying and creating microservices with little friction. No need to deal with a complex DSL or new programming language to get yourselves going with Microservices.
88
100
 
89
101
  ## More Info
90
102
 
91
103
  See [the wiki](https://github.com/stitchfix/stitches/wiki/Setup) for how to setup stitches.
92
104
 
93
- * [Stitches Features](https://github.com/stitchfix/stitches/wiki/Features-of-Stitches) include:
105
+ - [Stitches Features](https://github.com/stitchfix/stitches/wiki/Features-of-Stitches) include:
94
106
  - Authorization via API key
95
107
  - Versioned requests via HTTP content types
96
108
  - Structured Errors
97
109
  - ISO 8601-formatted dates
98
110
  - Deprecation using the `Sunset` header
99
- * The [Generator](https://github.com/stitchfix/stitches/wiki/Generator) sets up some code in your app, so you can start writing
100
- APIs using vanilla Rails idioms:
111
+ - An optional ApiKey cache to allow mostly DB free APIs
112
+ - The [Generator](https://github.com/stitchfix/stitches/wiki/Generator) sets up some code in your app, so you can start writing
113
+ APIs using vanilla Rails idioms:
101
114
  - a "ping" controller that can validate your app is working
102
115
  - version routing based on content-type (requests for V2 use the same URL, but are serviced by a different controller)
103
116
  - An ApiClient Active Record
104
117
  - Acceptance tests that can produce API documentation as they test your app.
105
- * Stitches provides [testing support](https://github.com/stitchfix/stitches/wiki/Testing)
118
+ - Stitches provides [testing support](https://github.com/stitchfix/stitches/wiki/Testing)
119
+
120
+ ## API Key Caching
121
+
122
+ Since version 4.0.0, stitches now has the ability to cache API keys in
123
+ memory for a configurable amount of time. This may be an improvement for
124
+ some applications.
125
+
126
+ You must configure the API Cache for it be used.
127
+
128
+ ```ruby
129
+ Stitches.configure do |config|
130
+ config.max_cache_ttl = 5 # seconds
131
+ config.max_cache_size = 100 # how many keys to cache
132
+ end
133
+ ```
106
134
 
135
+ Your cache size should be
136
+ larger then the number of consumer keys your service has.
107
137
 
108
138
  ## Developing
109
139
 
110
- Although `Stitches.configuration` is global, do not depend directly on that in your logic. Instead, allow all classes to receive a configuration object in their constructor. This makes the classes easier to deal with and change, without incurring much of a real cost to development. Global symbols suck, but are convenient. This is how you make the most of it.
140
+ Although `Stitches.configuration` is global, do not depend directly on that in your logic. Instead, allow all classes to receive a configuration object in their constructor. This makes the classes easier to deal with and change, without incurring much of a real cost to development. Global symbols suck, but are convenient. This is how you make the most of it.
111
141
 
112
142
  Also, the integration test does a lot of "testing the implementation", but since Rails generators are notorious for silently
113
- failing with a successful result, we have to make sure that the various `inject_into_file` calls are actually working. Do not do
143
+ failing with a successful result, we have to make sure that the various `inject_into_file` calls are actually working. Do not do
114
144
  any fancy refactors here, just keep it up to date.
115
145
 
116
146
  ---
@@ -2,7 +2,6 @@ module Stitches
2
2
  # A middleware that will skip its behavior if the path matches an allowed URL
3
3
  class AllowlistMiddleware
4
4
  def initialize(app, options={})
5
-
6
5
  @app = app
7
6
  @configuration = options[:configuration] || Stitches.configuration
8
7
  @except = options[:except] || @configuration.allowlist_regexp
@@ -0,0 +1,42 @@
1
+ require 'lru_redux'
2
+
3
+ module Stitches::ApiClientAccessWrapper
4
+
5
+ def self.fetch_for_key(key)
6
+ if cache_enabled
7
+ fetch_for_key_from_cache(key)
8
+ else
9
+ fetch_for_key_from_db(key)
10
+ end
11
+ end
12
+
13
+ def self.fetch_for_key_from_cache(key)
14
+ api_key_cache.getset(key) do
15
+ fetch_for_key_from_db(key)
16
+ end
17
+ end
18
+
19
+ def self.fetch_for_key_from_db(key)
20
+ if ::ApiClient.column_names.include?("enabled")
21
+ ::ApiClient.find_by(key: key, enabled: true)
22
+ else
23
+ ActiveSupport::Deprecation.warn('api_keys is missing "enabled" column. Run "rails g stitches:add_enabled_to_api_clients"')
24
+ ::ApiClient.find_by(key: key)
25
+ end
26
+ end
27
+
28
+ def self.clear_api_cache
29
+ api_key_cache.clear if cache_enabled
30
+ end
31
+
32
+ def self.api_key_cache
33
+ @api_key_cache ||= LruRedux::TTL::ThreadSafeCache.new(
34
+ Stitches.configuration.max_cache_size,
35
+ Stitches.configuration.max_cache_ttl,
36
+ )
37
+ end
38
+
39
+ def self.cache_enabled
40
+ Stitches.configuration.max_cache_ttl.positive?
41
+ end
42
+ end
@@ -12,7 +12,6 @@ module Stitches
12
12
 
13
13
  desc "Bootstraps your API service with a basic ping controller and spec to ensure everything is setup properly"
14
14
  def bootstrap_api
15
- gem "stitches"
16
15
  gem "apitome"
17
16
  gem_group :development, :test do
18
17
  gem "rspec"
@@ -20,7 +19,9 @@ module Stitches
20
19
  gem "rspec_api_documentation"
21
20
  end
22
21
 
23
- run "bundle install"
22
+ Bundler.with_clean_env do
23
+ run "bundle install"
24
+ end
24
25
  generate "apitome:install"
25
26
  generate "rspec:install"
26
27
 
@@ -20,11 +20,6 @@ module Stitches
20
20
  # ApiClient that it maps to.
21
21
  class ApiKey < Stitches::AllowlistMiddleware
22
22
 
23
- def initialize(app,options = {})
24
- super(app,options)
25
- @realm = Rails.application.class.parent.to_s
26
- end
27
-
28
23
  protected
29
24
 
30
25
  def do_call(env)
@@ -32,35 +27,40 @@ module Stitches
32
27
  if authorization
33
28
  if authorization =~ /#{@configuration.custom_http_auth_scheme}\s+key=(.*)\s*$/
34
29
  key = $1
35
-
36
- if ApiClient.column_names.include?("enabled")
37
- client = ApiClient.where(key: key, enabled: true).first
38
- else
39
- ActiveSupport::Deprecation.warn('api_keys is missing "enabled" column. Run "rails g stitches:add_enabled_to_api_clients"')
40
- client = ApiClient.where(key: key).first
41
- end
42
-
30
+ client = Stitches::ApiClientAccessWrapper.fetch_for_key(key)
43
31
  if client.present?
44
32
  env[@configuration.env_var_to_hold_api_client_primary_key] = client.id
45
33
  env[@configuration.env_var_to_hold_api_client] = client
46
34
  @app.call(env)
47
35
  else
48
- UnauthorizedResponse.new("key invalid",@realm,@configuration.custom_http_auth_scheme)
36
+ unauthorized_response("key invalid")
49
37
  end
50
38
  else
51
- UnauthorizedResponse.new("bad authorization type",@realm,@configuration.custom_http_auth_scheme)
39
+ unauthorized_response("bad authorization type")
52
40
  end
53
41
  else
54
- UnauthorizedResponse.new("no authorization header",@realm,@configuration.custom_http_auth_scheme)
42
+ unauthorized_response("no authorization header")
55
43
  end
56
44
  end
57
45
 
58
46
  private
59
47
 
60
- class UnauthorizedResponse < Rack::Response
61
- def initialize(reason,realm,custom_http_auth_scheme)
62
- super("Unauthorized - #{reason}", 401, { "WWW-Authenticate" => "#{custom_http_auth_scheme} realm=#{realm}" })
63
- end
48
+ # TODO: (jdlubrano)
49
+ # Once Rails 5 support is no longer necessary, we can simply call
50
+ # Rails.application.class.module_parent. The module_parent method
51
+ # does not exist in Rails <= 5, though, so we need to gracefully fallback
52
+ # Rails.application.class.parent for Rails versions predating Rails 6.0.0.
53
+ def rails_app_module
54
+ application_class = Rails.application.class
55
+ parent = application_class.try(:module_parent) || application_class.parent
56
+ parent.to_s
57
+ end
58
+
59
+ def unauthorized_response(reason)
60
+ status = 401
61
+ body = "Unauthorized - #{reason}"
62
+ header = { "WWW-Authenticate" => "#{@configuration.custom_http_auth_scheme} realm=#{rails_app_module}" }
63
+ Rack::Response.new(body, status, header).finish
64
64
  end
65
65
 
66
66
  end
@@ -13,6 +13,8 @@ class Stitches::Configuration
13
13
  @custom_http_auth_scheme = UnsetString.new("custom_http_auth_scheme")
14
14
  @env_var_to_hold_api_client_primary_key = NonNullString.new("env_var_to_hold_api_client_primary_key","STITCHES_API_CLIENT_ID")
15
15
  @env_var_to_hold_api_client= NonNullString.new("env_var_to_hold_api_client","STITCHES_API_CLIENT")
16
+ @max_cache_ttl = NonNullInteger.new("max_cache_ttl", 0)
17
+ @max_cache_size = NonNullInteger.new("max_cache_size", 0)
16
18
  end
17
19
 
18
20
  # A RegExp that allows URLS around the mime type and api key requirements.
@@ -25,11 +27,6 @@ class Stitches::Configuration
25
27
  @allowlist_regexp = new_allowlist_regexp
26
28
  end
27
29
 
28
- def whitelist_regexp=(new_allowlist_regexp)
29
- self.allowlist_regexp = new_allowlist_regexp
30
- warn("⚠️ 'whitelist' is deprecated in stitches configuration, please use 'allowlist' or auto-update with:\n\n bin/rails g stitches:update_configuration\n\n⚠️ 'whitelist' will be removed in 4.0")
31
- end
32
-
33
30
  # The name of your custom http auth scheme. This must be set, and has no default
34
31
  def custom_http_auth_scheme
35
32
  @custom_http_auth_scheme.to_s
@@ -39,7 +36,7 @@ class Stitches::Configuration
39
36
  @custom_http_auth_scheme = NonNullString.new("custom_http_auth_scheme",new_custom_http_auth_scheme)
40
37
  end
41
38
 
42
- # The name of the environment variable that the ApiKey middleware should use to
39
+ # The name of the environment variable that the ApiKey middleware should use to
43
40
  # place the primary key of the authenticated ApiKey. For example, if a user provides
44
41
  # the api key 1234-1234-1234-1234, and that maps to the primary key 42 in your database,
45
42
  # the environment will contain "42" in the key provided here.
@@ -59,8 +56,40 @@ class Stitches::Configuration
59
56
  @env_var_to_hold_api_client= NonNullString.new("env_var_to_hold_api_client",new_env_var_to_hold_api_client)
60
57
  end
61
58
 
59
+ def max_cache_ttl
60
+ @max_cache_ttl.to_i
61
+ end
62
+
63
+ def max_cache_ttl=(new_max_cache_ttl)
64
+ @max_cache_ttl = NonNullInteger.new("max_cache_ttl", new_max_cache_ttl)
65
+ end
66
+
67
+ def max_cache_size
68
+ @max_cache_size.to_i
69
+ end
70
+
71
+ def max_cache_size=(new_max_cache_size)
72
+ @max_cache_size = NonNullInteger.new("max_cache_size", new_max_cache_size)
73
+ end
74
+
62
75
  private
63
76
 
77
+ class NonNullInteger
78
+ def initialize(name, value)
79
+ unless value.is_a?(Integer)
80
+ raise "#{name} must be an Integer, not a #{value.class}"
81
+ end
82
+
83
+ @value = value
84
+ end
85
+
86
+ def to_i
87
+ @value
88
+ end
89
+
90
+ alias to_integer to_i
91
+ end
92
+
64
93
  class NonNullString
65
94
  def initialize(name,string)
66
95
  unless string.nil? || string.is_a?(String)
@@ -11,4 +11,14 @@ Stitches.configure do |configuration|
11
11
  # Env var that gets the primary key of the authenticated ApiKey
12
12
  # for access in your controllers, so they don't need to re-parse the header
13
13
  # configuration.env_var_to_hold_api_client_primary_key = "YOUR_ENV_VAR"
14
+
15
+ # Configures how long to cache ApiKeys in memory (In Seconds)
16
+ # A value of 0 will disable the cache entierly
17
+ # Default is 0
18
+ # configuration.max_cache_ttl = 5
19
+
20
+ # Configures how many ApiKeys to cache at one time
21
+ # This should be larger then the number of clients
22
+ # Default is 0
23
+ # configuration.max_cache_size = 100
14
24
  end
@@ -123,14 +123,22 @@ feature "general API stuff" do
123
123
  failure_message do
124
124
  correct_code,_ = evaluate_response(response)
125
125
  if correct_code
126
- "Expected WWW-Authenticate header to be 'CustomKeyAuth realm=#{Rails.application.class.parent.to_s}', but was #{response['WWW-Authenticate']}"
126
+ "Expected WWW-Authenticate header to be 'CustomKeyAuth realm=#{realm}', but was #{response['WWW-Authenticate']}"
127
127
  else
128
128
  "Expected response to be 401, but was #{response.response_code}"
129
129
  end
130
130
  end
131
131
 
132
+ def realm
133
+ <% if ::Rails::VERSION::MAJOR >= 6 -%>
134
+ Rails.application.class.module_parent.to_s
135
+ <% else %>
136
+ Rails.application.class.parent.to_s
137
+ <% end %>
138
+ end
139
+
132
140
  def evaluate_response(response)
133
- [response.response_code == 401, response.headers["WWW-Authenticate"] == "CustomKeyAuth realm=#{Rails.application.class.parent.to_s}" ]
141
+ [response.response_code == 401, response.headers["WWW-Authenticate"] == "CustomKeyAuth realm=#{realm}"]
134
142
  end
135
143
  end
136
144
  end
@@ -1,9 +1,11 @@
1
1
  require 'stitches/api_key'
2
2
  require 'stitches/valid_mime_type'
3
+ require 'stitches/api_client_access_wrapper'
3
4
 
4
5
  module Stitches
5
6
  class Railtie < Rails::Railtie
6
7
  config.app_middleware.use Stitches::ApiKey
7
8
  config.app_middleware.use Stitches::ValidMimeType
9
+
8
10
  end
9
11
  end
@@ -16,16 +16,17 @@ module Stitches
16
16
  if accept =~ %r{application/json} && accept =~ %r{version=\d+}
17
17
  @app.call(env)
18
18
  else
19
- NotAcceptableResponse.new(accept)
19
+ not_acceptable_response(accept)
20
20
  end
21
21
  end
22
22
 
23
23
  private
24
24
 
25
- class NotAcceptableResponse < Rack::Response
26
- def initialize(accept_header)
27
- super("Not Acceptable - '#{accept_header}' didn't have the right mime type or version number. We only accept application/json with a version", 406)
28
- end
25
+ def not_acceptable_response(accept_header)
26
+ status = 406
27
+ body = "Not Acceptable - '#{accept_header}' didn't have the right mime type or version number. We only accept application/json with a version"
28
+ header = { "WWW-Authenticate" => accept_header }
29
+ Rack::Response.new(body, status, header).finish
29
30
  end
30
31
 
31
32
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Stitches
2
- VERSION = '3.8.0'
4
+ VERSION = '4.0.0'
3
5
  end
@@ -14,7 +14,6 @@ require 'stitches/errors'
14
14
  require 'stitches/api_generator'
15
15
  require 'stitches/add_deprecation_generator'
16
16
  require 'stitches/add_enabled_to_api_clients_generator'
17
- require 'stitches/update_configuration_generator'
18
17
  require 'stitches/api_version_constraint'
19
18
  require 'stitches/api_key'
20
19
  require 'stitches/deprecation'
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "owners": [
3
3
  {
4
- "team": "platform"
4
+ "team": "devex"
5
5
  }
6
6
  ]
7
7
  }
@@ -0,0 +1,52 @@
1
+ require 'spec_helper.rb'
2
+
3
+ module MyApp
4
+ class Application
5
+ end
6
+ end
7
+
8
+ unless defined? ApiClient
9
+ class ApiClient
10
+ def self.column_names
11
+ ["enabled"]
12
+ end
13
+ end
14
+ end
15
+
16
+ describe Stitches::ApiClientAccessWrapper do
17
+ let(:api_client) {
18
+ double(ApiClient, id: 42)
19
+ }
20
+ before do
21
+ Stitches.configuration.reset_to_defaults!
22
+ end
23
+ describe '#fetch_by_key' do
24
+ context "cache is disabled" do
25
+ before do
26
+ expect(ApiClient).to receive(:find_by).and_return(api_client).twice
27
+ end
28
+
29
+ it "fetchs object from db twice" do
30
+ expect(described_class.fetch_for_key("123").id).to eq(42)
31
+ expect(described_class.fetch_for_key("123").id).to eq(42)
32
+ end
33
+ end
34
+
35
+ context "cache is configured" do
36
+ before do
37
+ Stitches.configure do |config|
38
+ config.max_cache_ttl = 5
39
+ config.max_cache_size = 10
40
+ end
41
+
42
+ expect(ApiClient).to receive(:find_by).and_return(api_client).once
43
+ end
44
+
45
+ it "fetchs object from cache" do
46
+ expect(described_class.fetch_for_key("123").id).to eq(42)
47
+ # This should hit the cache
48
+ expect(described_class.fetch_for_key("123").id).to eq(42)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -15,10 +15,8 @@ end
15
15
 
16
16
  describe Stitches::ApiKey do
17
17
  let(:app) { double("rack app") }
18
- let(:api_clients) {
19
- [
20
- double(ApiClient, id: 42)
21
- ]
18
+ let(:api_client) {
19
+ double(ApiClient, id: 42)
22
20
  }
23
21
 
24
22
  before do
@@ -27,23 +25,27 @@ describe Stitches::ApiKey do
27
25
  fake_rails_app = MyApp::Application.new
28
26
  allow(Rails).to receive(:application).and_return(fake_rails_app)
29
27
  allow(app).to receive(:call).with(env)
30
- allow(ApiClient).to receive(:where).and_return(api_clients)
28
+ allow(ApiClient).to receive(:find_by).and_return(api_client)
29
+ Stitches::ApiClientAccessWrapper.clear_api_cache
31
30
  end
32
31
 
33
32
  subject(:middleware) { described_class.new(app, namespace: "/api") }
34
33
 
35
34
  shared_examples "an unauthorized response" do
36
35
  it "returns a 401" do
37
- expect(@response.status).to eq(401)
36
+ status, _headers, _body = @response
37
+ expect(status).to eq(401)
38
38
  end
39
39
  it "sets the proper header" do
40
- expect(@response.headers["WWW-Authenticate"]).to eq("MyAwesomeInternalScheme realm=MyApp")
40
+ _status, headers, _body = @response
41
+ expect(headers["WWW-Authenticate"]).to eq("MyAwesomeInternalScheme realm=MyApp")
41
42
  end
42
43
  it "stops the call chain preventing anything from happening" do
43
44
  expect(app).not_to have_received(:call)
44
45
  end
45
46
  it "sends a reasonable message" do
46
- expect(@response.body).to eq([expected_body])
47
+ _status, _headers, body = @response
48
+ expect(body).to eq([expected_body])
47
49
  end
48
50
  end
49
51
 
@@ -155,18 +157,17 @@ describe Stitches::ApiKey do
155
157
  end
156
158
 
157
159
  it "sets the api_client's ID in the environment" do
158
- expect(env[Stitches.configuration.env_var_to_hold_api_client_primary_key]).to eq(api_clients.first.id)
160
+ expect(env[Stitches.configuration.env_var_to_hold_api_client_primary_key]).to eq(api_client.id)
159
161
  end
160
162
 
161
163
  it "sets the api_client itself in the environment" do
162
- expect(env[Stitches.configuration.env_var_to_hold_api_client]).to eq(api_clients.first)
164
+ expect(env[Stitches.configuration.env_var_to_hold_api_client]).to eq(api_client)
163
165
  end
164
166
  end
165
167
 
166
168
  context "unauthorized responses" do
167
169
  before do
168
170
  @response = middleware.call(env)
169
- @response.finish
170
171
  end
171
172
  context "invalid key" do
172
173
  let(:env) {
@@ -175,7 +176,7 @@ describe Stitches::ApiKey do
175
176
  "HTTP_AUTHORIZATION" => "MyAwesomeInternalScheme key=foobar",
176
177
  }
177
178
  }
178
- let(:api_clients) { [] }
179
+ let(:api_client) { nil }
179
180
 
180
181
  it_behaves_like "an unauthorized response" do
181
182
  let(:expected_body) { "Unauthorized - key invalid" }
@@ -9,17 +9,23 @@ describe Stitches::Configuration do
9
9
  let(:allowlist_regexp) { %r{foo} }
10
10
  let(:custom_http_auth_scheme) { "Blah" }
11
11
  let(:env_var_to_hold_api_client_primary_key) { "FOOBAR" }
12
+ let(:max_cache_ttl) { 11 }
13
+ let(:max_cache_size) { 111 }
12
14
 
13
15
  it "can be configured globally" do
14
16
  Stitches.configure do |config|
15
17
  config.allowlist_regexp = allowlist_regexp
16
18
  config.custom_http_auth_scheme = custom_http_auth_scheme
17
19
  config.env_var_to_hold_api_client_primary_key = env_var_to_hold_api_client_primary_key
20
+ config.max_cache_ttl = max_cache_ttl
21
+ config.max_cache_size = max_cache_size
18
22
  end
19
23
 
20
24
  expect(Stitches.configuration.allowlist_regexp).to eq(allowlist_regexp)
21
25
  expect(Stitches.configuration.custom_http_auth_scheme).to eq(custom_http_auth_scheme)
22
26
  expect(Stitches.configuration.env_var_to_hold_api_client_primary_key).to eq(env_var_to_hold_api_client_primary_key)
27
+ expect(Stitches.configuration.max_cache_ttl).to eq(max_cache_ttl)
28
+ expect(Stitches.configuration.max_cache_size).to eq(max_cache_size)
23
29
  end
24
30
 
25
31
  it "defaults to nil for allowlist_regexp" do
@@ -30,6 +36,14 @@ describe Stitches::Configuration do
30
36
  expect(Stitches.configuration.env_var_to_hold_api_client_primary_key).to eq("STITCHES_API_CLIENT_ID")
31
37
  end
32
38
 
39
+ it "defaults to 0 for max_cache_ttl" do
40
+ expect(Stitches.configuration.max_cache_ttl).to eq(0)
41
+ end
42
+
43
+ it "sets a default for max_cache_size" do
44
+ expect(Stitches.configuration.max_cache_size).to eq(0)
45
+ end
46
+
33
47
  it "blows up if you try to use custom_http_auth_scheme without having set it" do
34
48
  expect {
35
49
  Stitches.configuration.custom_http_auth_scheme
@@ -102,19 +116,34 @@ describe Stitches::Configuration do
102
116
  }.not_to raise_error
103
117
  end
104
118
  end
105
- context "deprecated options we want to support for backwards compatibility" do
106
119
 
107
- let(:logger) { double("logger") }
108
- before do
109
- allow(Rails).to receive(:logger).and_return(logger)
110
- allow(logger).to receive(:info)
120
+ describe "max_cache_ttl" do
121
+ let(:config) { Stitches::Configuration.new }
122
+ it "must be an integer" do
123
+ expect {
124
+ config.max_cache_ttl = ""
125
+ }.to raise_error(/max_cache_ttl must be an Integer, not a String/)
111
126
  end
112
127
 
113
- it "'whitelist' still works for allowlist" do
114
- Stitches.configure do |config|
115
- config.whitelist_regexp = /foo/
116
- end
117
- expect(Stitches.configuration.allowlist_regexp).to eq(/foo/)
128
+ it "may not be nil" do
129
+ expect {
130
+ config.max_cache_ttl = nil
131
+ }.to raise_error(/max_cache_ttl must be an Integer, not a NilClass/)
132
+ end
133
+ end
134
+
135
+ describe "max_cache_size" do
136
+ let(:config) { Stitches::Configuration.new }
137
+ it "must be an integer" do
138
+ expect {
139
+ config.max_cache_size = ""
140
+ }.to raise_error(/max_cache_size must be an Integer, not a String/)
141
+ end
142
+
143
+ it "may not be nil" do
144
+ expect {
145
+ config.max_cache_size = nil
146
+ }.to raise_error(/max_cache_size must be an Integer, not a NilClass/)
118
147
  end
119
148
  end
120
149
  end
@@ -109,35 +109,6 @@ RSpec.describe "Adding Stitches to a New Rails App", :integration do
109
109
  expect(include_line).to_not be_nil,lines.inspect
110
110
  end
111
111
 
112
- it "inserts can update old configuration" do
113
- run "bin/rails generate stitches:api"
114
-
115
- initializer = rails_root / "config" / "initializers" / "stitches.rb"
116
-
117
- initializer_contents = File.read(initializer).split(/\n/)
118
- found_initializer = false
119
- File.open(initializer,"w") do |file|
120
- initializer_contents.each do |line|
121
- if line =~ /allowlist/
122
- line = line.gsub("allowlist","whitelist")
123
- found_initializer = true
124
- end
125
- file.puts line
126
- end
127
- end
128
-
129
- raise "Didn't find 'allowlist' in the initializer?!" if !found_initializer
130
-
131
- run "bin/rails generate stitches:update_configuration"
132
-
133
- lines = File.read(initializer).split(/\n/)
134
- include_line = lines.detect { |line|
135
- line =~ /whitelist/
136
- }
137
-
138
- expect(include_line).to be_nil,lines.inspect
139
- end
140
-
141
112
  class RoutesFileAnalysis
142
113
  attr_reader :routes_file
143
114
  def initialize(routes_file, namespace: nil, module_scope: nil, resource: nil, mounted_engine: nil)
@@ -11,13 +11,15 @@ describe Stitches::ValidMimeType do
11
11
 
12
12
  shared_examples "an unacceptable response" do
13
13
  it "returns a 406" do
14
- expect(@response.status).to eq(406)
14
+ status, _headers, _body = @response
15
+ expect(status).to eq(406)
15
16
  end
16
17
  it "stops the call chain preventing anything from happening" do
17
18
  expect(app).not_to have_received(:call)
18
19
  end
19
20
  it "sends a reasonable message" do
20
- expect(@response.body.first).to match(/didn't have the right mime type or version number. We only accept application\/json/)
21
+ _status, _headers, body = @response
22
+ expect(body.first).to match(/didn't have the right mime type or version number. We only accept application\/json/)
21
23
  end
22
24
  end
23
25
 
@@ -133,7 +135,6 @@ describe Stitches::ValidMimeType do
133
135
  context "unacceptable responses" do
134
136
  before do
135
137
  @response = middleware.call(env)
136
- @response.finish
137
138
  end
138
139
  context "no header" do
139
140
  let(:env) {
@@ -20,6 +20,7 @@ Gem::Specification.new do |s|
20
20
 
21
21
  s.add_runtime_dependency("rails")
22
22
  s.add_runtime_dependency("pg")
23
+ s.add_runtime_dependency("lru_redux")
23
24
 
24
25
  s.add_development_dependency("rspec", ">= 3")
25
26
  s.add_development_dependency("rake")
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stitches
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.8.0
4
+ version: 4.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stitch Fix Engineering
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2019-08-07 00:00:00.000000000 Z
14
+ date: 2020-07-27 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: rails
@@ -41,6 +41,20 @@ dependencies:
41
41
  - - ">="
42
42
  - !ruby/object:Gem::Version
43
43
  version: '0'
44
+ - !ruby/object:Gem::Dependency
45
+ name: lru_redux
46
+ requirement: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: '0'
51
+ type: :runtime
52
+ prerelease: false
53
+ version_requirements: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
44
58
  - !ruby/object:Gem::Dependency
45
59
  name: rspec
46
60
  requirement: !ruby/object:Gem::Requirement
@@ -95,11 +109,12 @@ extensions: []
95
109
  extra_rdoc_files: []
96
110
  files:
97
111
  - ".circleci/config.yml"
112
+ - ".github/CODEOWNERS"
113
+ - ".github/PULL_REQUEST_TEMPLATE.md"
98
114
  - ".gitignore"
99
115
  - ".ruby-gemset"
100
116
  - ".ruby-version"
101
117
  - ".travis.yml"
102
- - CODEOWNERS
103
118
  - CODE_OF_CONDUCT.md
104
119
  - CONTRIBUTING.md
105
120
  - Gemfile
@@ -107,8 +122,8 @@ files:
107
122
  - Gemfile.rails-5.0
108
123
  - Gemfile.rails-5.1
109
124
  - Gemfile.rails-5.2
125
+ - Gemfile.rails-6.0
110
126
  - LICENSE.txt
111
- - PULL_REQUEST_TEMPLATE.md
112
127
  - README.md
113
128
  - Rakefile
114
129
  - build-matrix.json
@@ -116,6 +131,7 @@ files:
116
131
  - lib/stitches/add_deprecation_generator.rb
117
132
  - lib/stitches/add_enabled_to_api_clients_generator.rb
118
133
  - lib/stitches/allowlist_middleware.rb
134
+ - lib/stitches/api_client_access_wrapper.rb
119
135
  - lib/stitches/api_generator.rb
120
136
  - lib/stitches/api_key.rb
121
137
  - lib/stitches/api_version_constraint.rb
@@ -146,12 +162,11 @@ files:
146
162
  - lib/stitches/spec/have_api_error.rb
147
163
  - lib/stitches/spec/show_deprecation.rb
148
164
  - lib/stitches/spec/test_headers.rb
149
- - lib/stitches/update_configuration_generator.rb
150
165
  - lib/stitches/valid_mime_type.rb
151
166
  - lib/stitches/version.rb
152
- - lib/stitches/whitelisting_middleware.rb
153
167
  - lib/stitches_norailtie.rb
154
168
  - owners.json
169
+ - spec/api_client_access_wrapper_spec.rb
155
170
  - spec/api_key_spec.rb
156
171
  - spec/api_version_constraint_spec.rb
157
172
  - spec/configuration_spec.rb
@@ -184,11 +199,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
184
199
  - !ruby/object:Gem::Version
185
200
  version: '0'
186
201
  requirements: []
187
- rubygems_version: 3.0.3
202
+ rubygems_version: 3.1.2
188
203
  signing_key:
189
204
  specification_version: 4
190
205
  summary: You'll be in stitches at how easy it is to create a service at Stitch Fix
191
206
  test_files:
207
+ - spec/api_client_access_wrapper_spec.rb
192
208
  - spec/api_key_spec.rb
193
209
  - spec/api_version_constraint_spec.rb
194
210
  - spec/configuration_spec.rb
@@ -1,16 +0,0 @@
1
- require 'rails/generators'
2
-
3
- module Stitches
4
- class UpdateConfigurationGenerator < Rails::Generators::Base
5
- include Rails::Generators::Migration
6
-
7
- source_root(File.expand_path(File.join(File.dirname(__FILE__),"generator_files")))
8
-
9
- desc "Change your configuration to use 'allowlist' so you'll be ready for 4.x"
10
- def update_to_allowlist
11
- gsub_file "config/initializers/stitches.rb", /whitelist/, "allowlist"
12
- puts "🎉 You are now good to go!"
13
- end
14
-
15
- end
16
- end
@@ -1,5 +0,0 @@
1
- require_relative "allowlist_middleware"
2
-
3
- module Stitches
4
- WhitelistingMiddleware = AllowlistMiddleware
5
- end