api_keys 0.4.3 → 0.5.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.
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Migration to add the `restrictions` column to the api_keys table.
4
+ # This enables request restrictions: locking a key to specific web origins
5
+ # (with `*.` subdomain wildcards) and/or to specific IP addresses and CIDR
6
+ # ranges.
7
+ #
8
+ # Run this migration if you're upgrading from a version of api_keys that
9
+ # didn't have request restrictions support. The column defaults to an empty
10
+ # object, which means "unrestricted", so existing keys are unaffected.
11
+ class AddRestrictionsToApiKeys < ActiveRecord::Migration<%= migration_version %>
12
+ CONSTRAINT_NAME = "api_keys_restrictions_is_object"
13
+
14
+ def up
15
+ add_column :api_keys, :restrictions, json_column_type, default: {}, null: false
16
+ add_restrictions_object_constraint
17
+ end
18
+
19
+ def down
20
+ remove_column :api_keys, :restrictions
21
+ end
22
+
23
+ private
24
+
25
+ # Helper method to determine the appropriate JSON column type based on the database adapter.
26
+ # Uses :jsonb for PostgreSQL for better performance and indexing, :json otherwise.
27
+ def json_column_type
28
+ # Check connection availability for adapter name inspection
29
+ if ActiveRecord::Base.connection.adapter_name.downcase.include?('postgresql')
30
+ :jsonb
31
+ else
32
+ :json
33
+ end
34
+ rescue ActiveRecord::ConnectionNotEstablished
35
+ # Fallback during initial setup or if connection isn't available
36
+ :text
37
+ end
38
+
39
+ # JSON columns can also hold arrays and scalars. The application requires an
40
+ # object so damaged or validation-bypassing writes cannot erase a policy.
41
+ def add_restrictions_object_constraint
42
+ return unless connection.supports_check_constraints?
43
+
44
+ expression = case connection.adapter_name.downcase
45
+ when /postgres/
46
+ "jsonb_typeof(restrictions) = 'object'"
47
+ when /sqlite/
48
+ "json_valid(restrictions) AND json_type(restrictions) = 'object'"
49
+ when /mysql|trilogy/
50
+ "JSON_TYPE(restrictions) = 'OBJECT'"
51
+ end
52
+ add_check_constraint :api_keys, expression, name: CONSTRAINT_NAME if expression
53
+ end
54
+ end
@@ -2,6 +2,8 @@
2
2
 
3
3
  # Migration responsible for creating the core api_keys table.
4
4
  class CreateApiKeysTable < ActiveRecord::Migration<%= migration_version %>
5
+ RESTRICTIONS_CONSTRAINT_NAME = "api_keys_restrictions_is_object"
6
+
5
7
  def change
6
8
  primary_key_type, foreign_key_type = primary_and_foreign_key_types
7
9
 
@@ -30,6 +32,10 @@ class CreateApiKeysTable < ActiveRecord::Migration<%= migration_version %>
30
32
  # Optional freeform metadata for tagging
31
33
  t.send(json_column_type, :metadata, default: {}, null: false)
32
34
 
35
+ # Optional request restrictions: which web origins and IP addresses may
36
+ # present this key. An empty object means the key is unrestricted.
37
+ t.send(json_column_type, :restrictions, default: {}, null: false)
38
+
33
39
  # Optional auto-expiration timestamp
34
40
  t.datetime :expires_at
35
41
 
@@ -67,6 +73,8 @@ class CreateApiKeysTable < ActiveRecord::Migration<%= migration_version %>
67
73
  t.index :environment
68
74
  t.index [:owner_type, :owner_id, :key_type, :environment], name: "index_api_keys_owner_type_env"
69
75
  end
76
+
77
+ add_restrictions_object_constraint
70
78
  end
71
79
 
72
80
  private
@@ -94,4 +102,18 @@ class CreateApiKeysTable < ActiveRecord::Migration<%= migration_version %>
94
102
  # Fallback during initial setup or if connection isn't available
95
103
  :text
96
104
  end
105
+
106
+ def add_restrictions_object_constraint
107
+ return unless connection.supports_check_constraints?
108
+
109
+ expression = case connection.adapter_name.downcase
110
+ when /postgres/
111
+ "jsonb_typeof(restrictions) = 'object'"
112
+ when /sqlite/
113
+ "json_valid(restrictions) AND json_type(restrictions) = 'object'"
114
+ when /mysql|trilogy/
115
+ "JSON_TYPE(restrictions) = 'OBJECT'"
116
+ end
117
+ add_check_constraint :api_keys, expression, name: RESTRICTIONS_CONSTRAINT_NAME if expression
118
+ end
97
119
  end
@@ -139,23 +139,28 @@ ApiKeys.configure do |config|
139
139
  # - permissions: Scope ceiling - array of allowed scopes, or :all for unrestricted
140
140
  # - revocable: Whether keys of this type can be revoked/deleted (default: true)
141
141
  # - limit: Max keys of this type per owner per environment (nil = unlimited)
142
- # - public: If true AND revocable: false, stores plaintext token in metadata
143
- # so it can be viewed again in the dashboard. Use ONLY for publishable
144
- # keys designed to be embedded in distributed apps. Public types must
142
+ # - public: If true, stores plaintext token in metadata so it can be viewed
143
+ # again in the dashboard. Use ONLY for publishable keys designed
144
+ # to be embedded in distributed apps. Public types must
145
145
  # use a finite, non-empty permissions array (never :all). (default: false)
146
146
  # SECURITY: NEVER set public: true on secret keys!
147
+ # - restrictions: Which request-restriction kinds keys of this type may carry:
148
+ # any subset of [:origins, :ips]. Omitted = both allowed.
149
+ # `restrictions: []` forbids restrictions for this type.
150
+ # See "REQUEST RESTRICTIONS" below.
147
151
  #
148
152
  # config.key_types = {
149
153
  # publishable: {
150
154
  # prefix: "pk", # → pk_test_, pk_live_
151
155
  # permissions: %w[read validate], # Can ONLY have these scopes
152
- # revocable: false, # Cannot be revoked - protects deployed apps!
153
156
  # public: true, # Store token for later viewing in dashboard
154
- # limit: 1 # Only 1 publishable key per environment
157
+ # limit: 1, # Only 1 publishable key per environment
158
+ # restrictions: [:origins] # Browser keys lock to domains, not IPs
155
159
  # },
156
160
  # secret: {
157
161
  # prefix: "sk", # → sk_test_, sk_live_
158
- # permissions: :all # No scope restrictions
162
+ # permissions: :all, # No scope restrictions
163
+ # restrictions: [:ips] # Server keys lock to addresses, not domains
159
164
  # # revocable: true (default)
160
165
  # # public: false (default) - NEVER store secret keys!
161
166
  # # limit: nil (default = unlimited)
@@ -275,6 +280,36 @@ ApiKeys.configure do |config|
275
280
  # Default: true
276
281
  # config.https_strict_mode = true
277
282
 
283
+ # ============================================================================
284
+ # REQUEST RESTRICTIONS (origin and IP allowlists)
285
+ # ============================================================================
286
+ #
287
+ # Any key can be locked to the places it may be used from:
288
+ #
289
+ # user.create_api_key!(name: "Widget key", allowed_origins: "example.com, *.example.com")
290
+ # key.allowed_ips = "203.0.113.7, 10.0.0.0/8"
291
+ #
292
+ # Origins are matched against the browser's Origin header (falling back to
293
+ # Referer); IPs are matched with CIDR support. Within a list any entry
294
+ # admits the request; every list that is set must pass. Keys with no
295
+ # restrictions work from anywhere, so nothing changes until you opt in.
296
+ # Policy mismatches answer 403 with `origin_not_allowed` / `ip_not_allowed`.
297
+ # Malformed stored policy also fails closed with `restriction_misconfigured`.
298
+ #
299
+ # Requires the restrictions column:
300
+ # rails generate api_keys:add_restrictions && rails db:migrate
301
+ # ============================================================================
302
+
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 }
310
+ #
311
+ # config.client_ip_resolver = ->(request) { request.headers.fetch("CF-Connecting-IP") }
312
+
278
313
  # ============================================================================
279
314
  # BACKGROUND JOBS & CALLBACKS
280
315
  # ============================================================================
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.4.3
4
+ version: 0.5.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/_restriction_fields.html.erb
87
88
  - app/views/api_keys/keys/_secret_keys.html.erb
88
89
  - app/views/api_keys/keys/_show_token.html.erb
89
90
  - app/views/api_keys/keys/_token_display.html.erb
@@ -109,6 +110,7 @@ files:
109
110
  - lib/api_keys/logging.rb
110
111
  - lib/api_keys/models/api_key.rb
111
112
  - lib/api_keys/models/concerns/has_api_keys.rb
113
+ - lib/api_keys/restrictions.rb
112
114
  - lib/api_keys/services/authenticator.rb
113
115
  - lib/api_keys/services/digestor.rb
114
116
  - lib/api_keys/services/token_generator.rb
@@ -116,9 +118,11 @@ files:
116
118
  - lib/api_keys/version.rb
117
119
  - lib/generators/api_keys/add_authentication_index_generator.rb
118
120
  - lib/generators/api_keys/add_key_types_generator.rb
121
+ - lib/generators/api_keys/add_restrictions_generator.rb
119
122
  - lib/generators/api_keys/install_generator.rb
120
123
  - lib/generators/api_keys/templates/add_authentication_index_to_api_keys.rb.erb
121
124
  - lib/generators/api_keys/templates/add_key_types_to_api_keys.rb.erb
125
+ - lib/generators/api_keys/templates/add_restrictions_to_api_keys.rb.erb
122
126
  - lib/generators/api_keys/templates/create_api_keys_table.rb.erb
123
127
  - lib/generators/api_keys/templates/initializer.rb
124
128
  homepage: https://github.com/rameerez/api_keys