Skip to content

Commit cef2b5d

Browse files
Add disabled at (#101)
* Backfill tests Structure the code slightly differently in order to facilitate testing, and then test with a database and a full Rails stack for better assurance we have things working correctly. * Add usage of disabled_at to allow old keys When old keys are used, and assuming the disabled_at field is set, this allows those old keys to be successful, but warns that the key is disabled and will be removed soon. Use defaults for 6.0 so that test work for Rails 6 runner Update the generators to reduce whitespace Blank lines were generated by the generators in a few places, which cause some style format validators to fail, such as rubocop. * Update version to 4.2.0 Note that we are moving from 4.0.2 to 4.2.0. This is because there were a set of RC builds and tags for 4.1.0 which never made it to a 4.1.0 release. Nevertheless, it seems prudent to move to a new minor revision.
1 parent 0daccee commit cef2b5d

89 files changed

Lines changed: 2020 additions & 346 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pkg
22
spec/reports
3+
spec/fake_app/log/
34
.vimrc
45
*.sw?
56
.idea/

‎README.md‎

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Then, set it up:
3535

3636
### Upgrading from an older version
3737

38-
- When upgrading to version 4.0.0 you may now take advantage of an in-memory cache
38+
- When upgrading to version 4.0.0 and above you may now take advantage of an in-memory cache
3939

4040
You can enabled it like so
4141

@@ -46,18 +46,46 @@ Stitches.configure do |config|
4646
end
4747
```
4848

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:
49+
You can also set a leniency for disabled API keys, which will allow old API keys to continue to be used if they have a
50+
`disabled_at` field set as long as the leniency is not exceeded. Note that if the `disabled_at` field is not populated
51+
the behavior will remain the same as it always was, and the request will be denied when the `enabled` field is set to
52+
`true`. If Stitches allows a call due to leniency settings, a log message will be generated with a severity depending on
53+
how long ago the API key was disabled.
54+
55+
```ruby
56+
Stitches.configure do |config|
57+
config.disabled_key_leniency_in_seconds = 3 * 24 * 60 * 60 # Time in seconds, defaults to three days
58+
config.disabled_key_leniency_error_log_threshold_in_seconds = 2 * 24 * 60 * 60 # Time in seconds, defaults to two days
59+
end
60+
```
61+
62+
If a disabled key is used within the `disabled_key_leniency_in_seconds`, it will be allowed.
63+
64+
Anytime a disabled key is used a log will be generated. If it is before the
65+
`disabled_key_leniency_error_log_threshold_in_seconds` it will be a warning log message, if it is after that, it will be
66+
an error message. `disabled_key_leniency_error_log_threshold_in_seconds` should never be a greater number than
67+
`disabled_key_leniency_in_seconds`, as this provides an escallating series of warnings before finally disabling access.
68+
69+
- If you are upgrading from a version older than 3.3.0 you need to run three generators, two of which create database
70+
migrations on your `api_clients` table:
5171

5272
```
5373
> bin/rails generate stitches:add_enabled_to_api_clients
5474
> bin/rails generate stitches:add_deprecation
75+
> bin/rails generate stitches:add_disabled_at_to_api_clients
5576
```
5677

57-
- If you have a version lower than 3.6.0, you need to run one generator:
78+
- If you are upgrading from a version between 3.3.0 and 3.5.0 you need to run two generators:
5879

5980
```
6081
> bin/rails generate stitches:add_deprecation
82+
> bin/rails generate stitches:add_disabled_at_to_api_clients
83+
```
84+
85+
- If you are upgrading from a version between 3.6.0 and 4.0.2 you need to run one generator:
86+
87+
```
88+
> bin/rails generate stitches:add_disabled_at_to_api_clients
6189
```
6290

6391
## Example Microservice Endpoint
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
require 'rails/generators'
2+
3+
module Stitches
4+
class AddDisabledAtToApiClientsGenerator < Rails::Generators::Base
5+
include Rails::Generators::Migration
6+
7+
source_root(File.expand_path(File.join(File.dirname(__FILE__),"generator_files")))
8+
9+
def self.next_migration_number(path)
10+
Time.now.utc.strftime("%Y%m%d%H%M%S")
11+
end
12+
13+
desc "Upgrade your api_clients table so it includes the `disabled_at` field"
14+
def update_api_clients_table
15+
migration_template "db/migrate/add_disabled_at_to_api_clients.rb", "db/migrate/add_disabled_at_to_api_clients.rb"
16+
end
17+
end
18+
end

‎lib/stitches/allowlist_middleware.rb‎

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,14 @@ module Stitches
33
class AllowlistMiddleware
44
def initialize(app, options={})
55
@app = app
6-
@configuration = options[:configuration] || Stitches.configuration
7-
@except = options[:except] || @configuration.allowlist_regexp
6+
@configuration = options[:configuration]
7+
@except = options[:except]
88

9-
unless @except.nil? || @except.is_a?(Regexp)
10-
raise ":except must be a Regexp"
11-
end
9+
allowlist_regex
1210
end
11+
1312
def call(env)
14-
if @except && @except.match(env["PATH_INFO"])
13+
if allowlist_regex && allowlist_regex.match(env["PATH_INFO"])
1514
@app.call(env)
1615
else
1716
do_call(env)
@@ -24,5 +23,20 @@ def do_call(env)
2423
raise 'subclass must implement'
2524
end
2625

26+
def configuration
27+
@configuration || Stitches.configuration
28+
end
29+
30+
private
31+
32+
def allowlist_regex
33+
regex = @except || configuration.allowlist_regexp
34+
35+
if !regex.nil? && !regex.is_a?(Regexp)
36+
raise ":except must be a Regexp"
37+
end
38+
39+
regex
40+
end
2741
end
2842
end

‎lib/stitches/api_client_access_wrapper.rb‎

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,57 @@
22

33
module Stitches::ApiClientAccessWrapper
44

5-
def self.fetch_for_key(key)
5+
def self.fetch_for_key(key, configuration)
66
if cache_enabled
7-
fetch_for_key_from_cache(key)
7+
fetch_for_key_from_cache(key, configuration)
88
else
9-
fetch_for_key_from_db(key)
9+
fetch_for_key_from_db(key, configuration)
1010
end
1111
end
1212

13-
def self.fetch_for_key_from_cache(key)
13+
def self.fetch_for_key_from_cache(key, configuration)
1414
api_key_cache.getset(key) do
15-
fetch_for_key_from_db(key)
15+
fetch_for_key_from_db(key, configuration)
1616
end
1717
end
1818

19-
def self.fetch_for_key_from_db(key)
20-
if ::ApiClient.column_names.include?("enabled")
21-
::ApiClient.find_by(key: key, enabled: true)
19+
def self.fetch_for_key_from_db(key, configuration)
20+
api_client = ::ApiClient.find_by(key: key)
21+
return unless api_client
22+
23+
unless api_client.respond_to?(:enabled?)
24+
logger.warn('api_keys is missing "enabled" column. Run "rails g stitches:add_enabled_to_api_clients"')
25+
return api_client
26+
end
27+
28+
unless api_client.respond_to?(:disabled_at)
29+
logger.warn('api_keys is missing "disabled_at" column. Run "rails g stitches:add_disabled_at_to_api_clients"')
30+
end
31+
32+
return api_client if api_client.enabled?
33+
34+
disabled_at = api_client.respond_to?(:disabled_at) ? api_client.disabled_at : nil
35+
if disabled_at && disabled_at > configuration.disabled_key_leniency_in_seconds.seconds.ago
36+
message = "Allowing disabled ApiClient: #{api_client.name} with key #{api_client.key} disabled at #{disabled_at}"
37+
if disabled_at > configuration.disabled_key_leniency_error_log_threshold_in_seconds.seconds.ago
38+
logger.warn(message)
39+
else
40+
logger.error(message)
41+
end
42+
return api_client
43+
else
44+
logger.error("Rejecting disabled ApiClient: #{api_client.name} with key #{api_client.key}")
45+
end
46+
nil
47+
end
48+
49+
def self.logger
50+
if defined?(StitchFix::Logger::LogWriter)
51+
StitchFix::Logger::LogWriter
52+
elsif defined?(Rails.logger)
53+
Rails.logger
2254
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)
55+
::Logger.new('/dev/null')
2556
end
2657
end
2758

@@ -39,4 +70,4 @@ def self.api_key_cache
3970
def self.cache_enabled
4071
Stitches.configuration.max_cache_ttl.positive?
4172
end
42-
end
73+
end

‎lib/stitches/api_key.rb‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@ class ApiKey < Stitches::AllowlistMiddleware
2525
def do_call(env)
2626
authorization = env["HTTP_AUTHORIZATION"]
2727
if authorization
28-
if authorization =~ /#{@configuration.custom_http_auth_scheme}\s+key=(.*)\s*$/
28+
if authorization =~ /#{configuration.custom_http_auth_scheme}\s+key=(.*)\s*$/
2929
key = $1
30-
client = Stitches::ApiClientAccessWrapper.fetch_for_key(key)
30+
client = Stitches::ApiClientAccessWrapper.fetch_for_key(key, configuration)
3131
if client.present?
32-
env[@configuration.env_var_to_hold_api_client_primary_key] = client.id
33-
env[@configuration.env_var_to_hold_api_client] = client
32+
env[configuration.env_var_to_hold_api_client_primary_key] = client.id
33+
env[configuration.env_var_to_hold_api_client] = client
3434
@app.call(env)
3535
else
3636
unauthorized_response("key invalid")
@@ -59,7 +59,7 @@ def rails_app_module
5959
def unauthorized_response(reason)
6060
status = 401
6161
body = "Unauthorized - #{reason}"
62-
header = { "WWW-Authenticate" => "#{@configuration.custom_http_auth_scheme} realm=#{rails_app_module}" }
62+
header = { "WWW-Authenticate" => "#{configuration.custom_http_auth_scheme} realm=#{rails_app_module}" }
6363
Rack::Response.new(body, status, header).finish
6464
end
6565

‎lib/stitches/configuration.rb‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,12 @@ def reset_to_defaults!
1515
@env_var_to_hold_api_client= NonNullString.new("env_var_to_hold_api_client","STITCHES_API_CLIENT")
1616
@max_cache_ttl = NonNullInteger.new("max_cache_ttl", 0)
1717
@max_cache_size = NonNullInteger.new("max_cache_size", 0)
18+
@disabled_key_leniency_in_seconds = ActiveSupport::Duration.days(3)
19+
@disabled_key_leniency_error_log_threshold_in_seconds = ActiveSupport::Duration.days(2)
1820
end
1921

22+
attr_accessor :disabled_key_leniency_in_seconds, :disabled_key_leniency_error_log_threshold_in_seconds
23+
2024
# A RegExp that allows URLS around the mime type and api key requirements.
2125
# nil means that ever request must have a proper mime type and api key.
2226
attr_reader :allowlist_regexp
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
class AddDisabledAtToApiClients < ActiveRecord::Migration<% if Rails::VERSION::MAJOR >= 5 %>[<%= Rails::VERSION::MAJOR %>.<%= Rails::VERSION::MINOR %>]<% end %>
2+
def change
3+
add_column :api_clients, :disabled_at, "timestamp with time zone", null: true
4+
end
5+
end

‎lib/stitches/generator_files/db/migrate/add_enabled_to_api_clients.rb‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
<% if Rails::VERSION::MAJOR >= 5 %>
2-
class AddEnabledToApiClients < ActiveRecord::Migration[<%= Rails::VERSION::MAJOR %>.<%= Rails::VERSION::MINOR %>]
3-
<% else %>
4-
class AddEnabledToApiClients < ActiveRecord::Migration
5-
<% end %>
1+
class AddEnabledToApiClients < ActiveRecord::Migration<% if Rails::VERSION::MAJOR >= 5 %>[<%= Rails::VERSION::MAJOR %>.<%= Rails::VERSION::MINOR %>]<% end %>
62
def change
73
add_column :api_clients, :enabled, :bool, null: false, default: true
84
remove_index :api_clients, [:name ] # existing one would be unique

‎lib/stitches/generator_files/db/migrate/create_api_clients.rb‎

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
1-
<% if Rails::VERSION::MAJOR >= 5 %>
2-
class CreateApiClients < ActiveRecord::Migration[<%= Rails::VERSION::MAJOR %>.<%= Rails::VERSION::MINOR %>]
3-
<% else %>
4-
class CreateApiClients < ActiveRecord::Migration
5-
<% end %>
1+
class CreateApiClients < ActiveRecord::Migration<% if Rails::VERSION::MAJOR >= 5 %>[<%= Rails::VERSION::MAJOR %>.<%= Rails::VERSION::MINOR %>]<% end %>
62
def change
73
create_table :api_clients do |t|
84
t.string :name, null: false
95
t.column :key, "uuid default uuid_generate_v4()", null: false
106
t.column :enabled, :bool, null: false, default: true
117
t.column :created_at, "timestamp with time zone default now()", null: false
8+
t.column :disabled_at, "timestamp with time zone", null: true
129
end
1310
add_index :api_clients, [:name]
1411
add_index :api_clients, [:key], unique: true

0 commit comments

Comments
 (0)