stitches 3.8.0 → 4.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.
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