api_keys 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +54 -0
  3. data/README.md +158 -9
  4. data/SECURITY.md +22 -2
  5. data/app/controllers/api_keys/keys_controller.rb +136 -14
  6. data/app/views/api_keys/keys/_form.html.erb +85 -17
  7. data/app/views/api_keys/keys/_key_row.html.erb +4 -2
  8. data/app/views/api_keys/keys/_refusal_notice.html.erb +24 -0
  9. data/app/views/api_keys/keys/_restriction_fields.html.erb +4 -2
  10. data/app/views/api_keys/keys/_show_token.html.erb +4 -7
  11. data/app/views/api_keys/keys/edit.html.erb +2 -0
  12. data/app/views/api_keys/security/best_practices.html.erb +1 -1
  13. data/app/views/layouts/api_keys/application.html.erb +6 -0
  14. data/docs/reviews/2026-10-18-0-5-1/01-astra-review-of-0-5-0.md +55 -0
  15. data/docs/reviews/2026-10-18-0-5-1/02-astra-re-review-of-0-5-1.md +36 -0
  16. data/docs/reviews/2026-10-18-0-5-1/03-opus-review-of-0-5-1.md +5 -0
  17. data/docs/reviews/2026-10-18-0-6-0/01-astra-review-of-0-6-0.md +36 -0
  18. data/docs/reviews/2026-10-18-0-6-0/02-astra-re-review-of-0-6-0.md +34 -0
  19. data/docs/reviews/2026-10-18-0-6-0/03-sol-re-check-of-0-6-0.md +41 -0
  20. data/lib/api_keys/authentication.rb +123 -7
  21. data/lib/api_keys/configuration.rb +37 -6
  22. data/lib/api_keys/form_builder_extensions.rb +42 -3
  23. data/lib/api_keys/helpers/token_session.rb +257 -8
  24. data/lib/api_keys/helpers/view_helpers.rb +33 -0
  25. data/lib/api_keys/jobs/update_stats_job.rb +45 -1
  26. data/lib/api_keys/models/api_key.rb +293 -7
  27. data/lib/api_keys/models/concerns/has_api_keys.rb +10 -2
  28. data/lib/api_keys/restrictions.rb +75 -24
  29. data/lib/api_keys/services/authenticator.rb +24 -2
  30. data/lib/api_keys/version.rb +1 -1
  31. data/lib/api_keys.rb +7 -0
  32. data/lib/generators/api_keys/add_key_activity_generator.rb +59 -0
  33. data/lib/generators/api_keys/add_restrictions_generator.rb +4 -1
  34. data/lib/generators/api_keys/templates/add_key_activity_to_api_keys.rb.erb +37 -0
  35. data/lib/generators/api_keys/templates/create_api_keys_table.rb.erb +12 -0
  36. data/lib/generators/api_keys/templates/initializer.rb +32 -7
  37. metadata +10 -1
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/base"
4
+ require "rails/generators/active_record"
5
+
6
+ module ApiKeys
7
+ module Generators
8
+ # Rails generator for the columns api_keys 0.6.0 adds to the api_keys
9
+ # table, in one migration:
10
+ #
11
+ # - `last_refused_at`, `refusals_count`, `last_refusal_code`: requests that
12
+ # presented a key's real token and were refused by a policy.
13
+ # - `token_shown_at`: when the key's token was released through the
14
+ # one-time dashboard handoff, which makes that handoff durable.
15
+ #
16
+ # This generator is for existing installations; new installs get the
17
+ # columns from the install generator, so they never need to run this.
18
+ class AddKeyActivityGenerator < Rails::Generators::Base
19
+ include ActiveRecord::Generators::Migration
20
+
21
+ source_root File.expand_path("templates", __dir__)
22
+
23
+ # Implement the required interface for Rails::Generators::Migration.
24
+ def self.next_migration_number(dirname)
25
+ next_migration_number = current_migration_number(dirname) + 1
26
+ ActiveRecord::Migration.next_migration_number(next_migration_number)
27
+ end
28
+
29
+ # Creates the migration file using the template.
30
+ def create_migration_file
31
+ migration_template "add_key_activity_to_api_keys.rb.erb",
32
+ File.join(db_migrate_path, "add_key_activity_to_api_keys.rb")
33
+ end
34
+
35
+ # Displays helpful information to the user after installation.
36
+ def display_post_install_message
37
+ say "\n🚦 api_keys 0.6.0 key activity migration created!", :green
38
+ say "\nNext steps:"
39
+ say " 1. Run `rails db:migrate` to add last_refused_at, refusals_count,"
40
+ say " last_refusal_code and token_shown_at."
41
+ say "\n 2. That's it. Requests that present a key's real token and are refused"
42
+ say " (origin/IP restrictions, environment, missing scope, revoked, expired...)"
43
+ say " are now stamped on the key, through the same background stats job as"
44
+ say " last_used_at, and the dashboard shows them on the key's page."
45
+ say " A refusal never moves last_used_at or requests_count."
46
+ say " refusals_count, like requests_count, is only kept with `config.track_requests_count = true`."
47
+ say "\n The one-time token page now records the first retrieval in token_shown_at,"
48
+ say " so a copied session cannot show a token twice, even after a cache flush."
49
+ say "\nSee the api_keys README for detailed usage and examples.", :cyan
50
+ end
51
+
52
+ private
53
+
54
+ def migration_version
55
+ "[#{ActiveRecord::VERSION::STRING.to_f}]"
56
+ end
57
+ end
58
+ end
59
+ end
@@ -41,8 +41,11 @@ module ApiKeys
41
41
  say " restrictions: [:origins] },"
42
42
  say " secret: { prefix: 'sk', permissions: :all, restrictions: [:ips] }"
43
43
  say " }"
44
- say "\n 4. Behind a CDN or proxy, make sure the client IP is truthful:"
44
+ say "\n 4. Make sure the client IP is truthful before relying on allowed_ips:"
45
+ say " # Behind a proxy that is the only way in: list it in trusted_proxies, keep the default."
45
46
  say " config.action_dispatch.trusted_proxies = ..."
47
+ say " # Rails directly reachable: check the socket peer, or X-Forwarded-For can be forged."
48
+ say " config.client_ip_resolver = ->(request) { request.remote_addr }"
46
49
  say " # Trust a vendor header only when ingress blocks requests that bypass that vendor."
47
50
  say "\nSee the api_keys README for detailed usage and examples.", :cyan
48
51
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Migration to add the api_keys 0.6.0 columns to the api_keys table.
4
+ #
5
+ # `last_refused_at`, `refusals_count`, and `last_refusal_code` record requests
6
+ # that presented a key's real token and were refused by a policy (origin or IP
7
+ # restrictions, environment isolation, missing scope, revoked, expired...), so
8
+ # the dashboard can say "requests are arriving but refused" instead of showing
9
+ # a key nobody ever tried. They never touch `last_used_at` or `requests_count`.
10
+ #
11
+ # `token_shown_at` records when the key's token was released through the
12
+ # one-time dashboard handoff. The first retrieval claims it with a single
13
+ # conditional UPDATE, so a copy of the session cannot show the token again,
14
+ # even after the cache is flushed. Existing keys keep NULL.
15
+ #
16
+ # Run this migration if you're upgrading from a version of api_keys before
17
+ # 0.6.0. New installs get the columns from the install generator. Until it
18
+ # runs, authentication works unchanged, refusals are simply not recorded, and
19
+ # the token handoff relies on the cache marker alone, as in 0.5.1. The
20
+ # migration is idempotent: columns that already exist are left alone.
21
+ class AddKeyActivityToApiKeys < ActiveRecord::Migration<%= migration_version %>
22
+ def up
23
+ add_column :api_keys, :last_refused_at, :datetime unless column_exists?(:api_keys, :last_refused_at)
24
+ unless column_exists?(:api_keys, :refusals_count)
25
+ add_column :api_keys, :refusals_count, :bigint, default: 0, null: false
26
+ end
27
+ add_column :api_keys, :last_refusal_code, :string, limit: 64 unless column_exists?(:api_keys, :last_refusal_code)
28
+ add_column :api_keys, :token_shown_at, :datetime unless column_exists?(:api_keys, :token_shown_at)
29
+ end
30
+
31
+ def down
32
+ remove_column :api_keys, :token_shown_at if column_exists?(:api_keys, :token_shown_at)
33
+ remove_column :api_keys, :last_refusal_code if column_exists?(:api_keys, :last_refusal_code)
34
+ remove_column :api_keys, :refusals_count if column_exists?(:api_keys, :refusals_count)
35
+ remove_column :api_keys, :last_refused_at if column_exists?(:api_keys, :last_refused_at)
36
+ end
37
+ end
@@ -45,6 +45,18 @@ class CreateApiKeysTable < ActiveRecord::Migration<%= migration_version %>
45
45
  # Optional counter cache for total requests made with this key.
46
46
  t.bigint :requests_count, default: 0, null: false
47
47
 
48
+ # Refused-but-identified traffic: requests that presented this key's real
49
+ # token and were refused by a policy (origin/IP lock, environment, scope,
50
+ # revoked, expired...). Kept apart from last_used_at, which a refusal never moves.
51
+ t.datetime :last_refused_at
52
+ t.bigint :refusals_count, default: 0, null: false
53
+ t.string :last_refusal_code, limit: 64
54
+
55
+ # When this key's token was released through the one-time dashboard
56
+ # handoff. The first retrieval claims it with a conditional UPDATE, so a
57
+ # copied session can never show the token twice.
58
+ t.datetime :token_shown_at
59
+
48
60
  # Timestamp when the key was revoked (null means active)
49
61
  t.datetime :revoked_at
50
62
 
@@ -148,11 +148,20 @@ ApiKeys.configure do |config|
148
148
  # any subset of [:origins, :ips]. Omitted = both allowed.
149
149
  # `restrictions: []` forbids restrictions for this type.
150
150
  # See "REQUEST RESTRICTIONS" below.
151
+ # - scopes: Where scope checks look. :stored (default) uses the scopes each
152
+ # key saved at creation, so WIDENING `permissions` LATER DOES NOT
153
+ # REACH EXISTING KEYS: they keep the old list and 403 new scopes.
154
+ # :ceiling makes the live `permissions` list the contract for
155
+ # every key of the type (widening and narrowing apply at once).
156
+ # Use it for types whose users do not pick scopes; it requires a
157
+ # finite, non-empty permissions list. For :stored types, backfill
158
+ # after widening with ApiKeys.reconcile_scopes!(key_type: :name).
151
159
  #
152
160
  # config.key_types = {
153
161
  # publishable: {
154
162
  # prefix: "pk", # → pk_test_, pk_live_
155
163
  # permissions: %w[read validate], # Can ONLY have these scopes
164
+ # scopes: :ceiling, # Every pk key has exactly these, even after edits
156
165
  # public: true, # Store token for later viewing in dashboard
157
166
  # limit: 1, # Only 1 publishable key per environment
158
167
  # restrictions: [:origins] # Browser keys lock to domains, not IPs
@@ -300,14 +309,22 @@ ApiKeys.configure do |config|
300
309
  # rails generate api_keys:add_restrictions && rails db:migrate
301
310
  # ============================================================================
302
311
 
303
- # How the client IP is resolved for `allowed_ips` checks.
304
- # The default trusts Rails' own resolution, which honors
305
- # config.action_dispatch.trusted_proxies. Behind a CDN that terminates the
306
- # connection, configure trusted_proxies whenever possible. Trust a vendor
307
- # header directly only when your network ingress rejects requests that
308
- # bypass that vendor; otherwise callers can spoof the address being checked.
309
- # Default: ->(request) { request.remote_ip }
312
+ # How the client IP is resolved for `allowed_ips` checks. Pick the line that
313
+ # matches how clients reach this Rails server:
310
314
  #
315
+ # Behind a load balancer, reverse proxy, or CDN that is the ONLY way in and
316
+ # that overwrites or appends X-Forwarded-For, with its addresses covered by
317
+ # config.action_dispatch.trusted_proxies: keep the default.
318
+ # config.client_ip_resolver = ->(request) { request.remote_ip } # default
319
+ #
320
+ # Directly reachable (no proxy, or a proxy that can be bypassed): check the
321
+ # socket peer. With remote_ip, a client could send
322
+ # `X-Forwarded-For: <an allowed address>` and pass the check.
323
+ # config.client_ip_resolver = ->(request) { request.remote_addr }
324
+ #
325
+ # trusted_proxies alone is not a boundary: it does not stop a client that
326
+ # bypasses the proxy from sending the header. Trust a vendor header only
327
+ # when your network ingress rejects requests that bypass that vendor:
311
328
  # config.client_ip_resolver = ->(request) { request.headers.fetch("CF-Connecting-IP") }
312
329
 
313
330
  # ============================================================================
@@ -336,9 +353,17 @@ ApiKeys.configure do |config|
336
353
 
337
354
  # Minimum interval between last_used_at jobs when exact request counting is
338
355
  # disabled. Set to 0 or nil to update on every successful authentication.
356
+ # The same interval debounces refusal stats (below).
339
357
  # Default: 1.minute
340
358
  # config.stats_update_interval = 1.minute
341
359
 
360
+ # Refused requests: when a request presents a key's real token and a policy
361
+ # refuses it (origin/IP lock, environment, missing scope, revoked, expired),
362
+ # the same stats job stamps last_refused_at and last_refusal_code (and
363
+ # refusals_count when track_requests_count is on). A refusal never moves
364
+ # last_used_at or requests_count. The dashboard shows it on the key's page.
365
+ # Existing installs: rails generate api_keys:add_key_activity && rails db:migrate
366
+
342
367
  # Queue names for background jobs.
343
368
  # Default: :default
344
369
  # config.stats_job_queue = :default
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: api_keys
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - rameerez
@@ -84,6 +84,7 @@ files:
84
84
  - app/views/api_keys/keys/_key_status.html.erb
85
85
  - app/views/api_keys/keys/_keys_table.html.erb
86
86
  - app/views/api_keys/keys/_publishable_keys.html.erb
87
+ - app/views/api_keys/keys/_refusal_notice.html.erb
87
88
  - app/views/api_keys/keys/_restriction_fields.html.erb
88
89
  - app/views/api_keys/keys/_secret_keys.html.erb
89
90
  - app/views/api_keys/keys/_show_token.html.erb
@@ -95,6 +96,12 @@ files:
95
96
  - app/views/api_keys/security/best_practices.html.erb
96
97
  - app/views/layouts/api_keys/application.html.erb
97
98
  - config/routes.rb
99
+ - docs/reviews/2026-10-18-0-5-1/01-astra-review-of-0-5-0.md
100
+ - docs/reviews/2026-10-18-0-5-1/02-astra-re-review-of-0-5-1.md
101
+ - docs/reviews/2026-10-18-0-5-1/03-opus-review-of-0-5-1.md
102
+ - docs/reviews/2026-10-18-0-6-0/01-astra-review-of-0-6-0.md
103
+ - docs/reviews/2026-10-18-0-6-0/02-astra-re-review-of-0-6-0.md
104
+ - docs/reviews/2026-10-18-0-6-0/03-sol-re-check-of-0-6-0.md
98
105
  - lib/api_keys.rb
99
106
  - lib/api_keys/authentication.rb
100
107
  - lib/api_keys/configuration.rb
@@ -117,10 +124,12 @@ files:
117
124
  - lib/api_keys/tenant_resolution.rb
118
125
  - lib/api_keys/version.rb
119
126
  - lib/generators/api_keys/add_authentication_index_generator.rb
127
+ - lib/generators/api_keys/add_key_activity_generator.rb
120
128
  - lib/generators/api_keys/add_key_types_generator.rb
121
129
  - lib/generators/api_keys/add_restrictions_generator.rb
122
130
  - lib/generators/api_keys/install_generator.rb
123
131
  - lib/generators/api_keys/templates/add_authentication_index_to_api_keys.rb.erb
132
+ - lib/generators/api_keys/templates/add_key_activity_to_api_keys.rb.erb
124
133
  - lib/generators/api_keys/templates/add_key_types_to_api_keys.rb.erb
125
134
  - lib/generators/api_keys/templates/add_restrictions_to_api_keys.rb.erb
126
135
  - lib/generators/api_keys/templates/create_api_keys_table.rb.erb