api_keys 0.6.0 → 0.7.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,52 @@
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 Idempotency-Keys table (api_keys 0.7.0), which
9
+ # ApiKeys::Idempotency needs: each write under an Idempotency-Key runs
10
+ # once per API key, and a retry answers the record as it is now. New and
11
+ # existing installations run it alike.
12
+ class AddIdempotencyKeysGenerator < Rails::Generators::Base
13
+ include ActiveRecord::Generators::Migration
14
+
15
+ source_root File.expand_path("templates", __dir__)
16
+
17
+ # Implement the required interface for Rails::Generators::Migration.
18
+ def self.next_migration_number(dirname)
19
+ next_migration_number = current_migration_number(dirname) + 1
20
+ ActiveRecord::Migration.next_migration_number(next_migration_number)
21
+ end
22
+
23
+ # Creates the migration file using the template.
24
+ def create_migration_file
25
+ migration_template "create_api_keys_idempotency_keys.rb.erb",
26
+ File.join(db_migrate_path, "create_api_keys_idempotency_keys.rb")
27
+ end
28
+
29
+ # Displays helpful information to the user after installation.
30
+ def display_post_install_message
31
+ say "\nšŸ” api_keys Idempotency-Keys migration created!", :green
32
+ say "\nNext steps:"
33
+ say " 1. Run `rails db:migrate` to create the api_keys_idempotency_keys table."
34
+ say "\n 2. In your API base controller, after authenticate_api_key!:"
35
+ say " include ApiKeys::Idempotency"
36
+ say " around_action :run_once_per_idempotency_key"
37
+ say "\n 3. Answer every write with render_resource(record, status: ...), and define"
38
+ say " resource_json(record, **options) to render a record and"
39
+ say " idempotent_replay_allowed?(record) to say whether current_api_key may read it."
40
+ say " Keep the around_action outside any transaction your app opens."
41
+ say "\n 4. Run ApiKeys::IdempotencyKey.prune! once a day."
42
+ say "\nSee the api_keys README for detailed usage and examples.", :cyan
43
+ end
44
+
45
+ private
46
+
47
+ def migration_version
48
+ "[#{ActiveRecord::VERSION::STRING.to_f}]"
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Migration to add the person behind each key to the api_keys table
4
+ # (api_keys 0.7.0): `created_by_type` and `created_by_id`, a polymorphic
5
+ # reference, for keys whose owner is not a person (an organization).
6
+ # `create_api_key!(created_by: current_user)` sets it, and
7
+ # `config.trusted_key_check` can refuse a key once that person no longer
8
+ # stands behind it.
9
+ #
10
+ # Run this migration if you're upgrading from a version of api_keys before
11
+ # 0.7.0. New installs get the columns from the install generator. Existing
12
+ # keys keep NULL. The migration is idempotent: columns that already exist are
13
+ # left alone.
14
+ class AddCreatedByToApiKeys < ActiveRecord::Migration<%= migration_version %>
15
+ def up
16
+ return if column_exists?(:api_keys, :created_by_id)
17
+
18
+ add_reference :api_keys, :created_by, polymorphic: true, null: true, type: foreign_key_type
19
+ end
20
+
21
+ def down
22
+ remove_reference :api_keys, :created_by, polymorphic: true if column_exists?(:api_keys, :created_by_id)
23
+ end
24
+
25
+ private
26
+
27
+ # The key type the install generator gives api_keys.owner_id: the app's
28
+ # `primary_key_type` generator setting, else :bigint.
29
+ def foreign_key_type
30
+ generators = Rails.application&.config&.generators
31
+ generators&.options&.dig(generators.orm, :primary_key_type) || :bigint
32
+ end
33
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Migration to create the table of device authorizations (api_keys 0.7.0).
4
+ # Each row is a client's request for an API key on the OAuth 2.0 Device
5
+ # Authorization Grant (RFC 8628), which a person approves or denies in your
6
+ # app (ApiKeys::DeviceFlow, ApiKeys::DeviceAuthorization). Both codes are
7
+ # kept as SHA-256 digests, never as text, and the key's token is never
8
+ # stored.
9
+ #
10
+ # Only apps that use the device flow need this table.
11
+ class CreateApiKeysDeviceAuthorizations < ActiveRecord::Migration<%= migration_version %>
12
+ def change
13
+ primary_key_type, foreign_key_type = primary_and_foreign_key_types
14
+
15
+ create_table :api_keys_device_authorizations, id: primary_key_type do |t|
16
+ t.string :device_code_digest, null: false, index: { unique: true }
17
+ t.string :user_code_digest, null: false, index: { unique: true }
18
+ # The client names itself, and every poll repeats it (RFC 8628 §3.4).
19
+ t.string :client_id, null: false
20
+ t.string :device_name
21
+ t.string :requested_from
22
+ # No default here: MySQL refuses a literal default on a JSON column.
23
+ # The model defaults scopes to [].
24
+ t.send(json_column_type, :scopes, null: false)
25
+ # The type of the key the approval makes, when key types are configured.
26
+ t.string :key_type
27
+ t.string :state, default: "pending", null: false
28
+ t.integer :interval, default: 5, null: false
29
+ t.datetime :expires_at, null: false, index: true
30
+ t.datetime :last_polled_at
31
+ # Who the key is for, and who decided: set when the person decides.
32
+ t.references :owner, polymorphic: true, null: true, type: foreign_key_type
33
+ t.references :decided_by, polymorphic: true, null: true, type: foreign_key_type
34
+ t.datetime :decided_at
35
+ # The key that the first poll after the approval created.
36
+ t.references :api_key, null: true, type: api_key_id_type, foreign_key: { on_delete: :nullify }
37
+ t.timestamps
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ # The key types the install generator gives the api_keys table: the app's
44
+ # `primary_key_type` generator setting, else Rails' defaults.
45
+ def primary_and_foreign_key_types
46
+ generators = Rails.application&.config&.generators
47
+ setting = generators&.options&.dig(generators.orm, :primary_key_type)
48
+ [setting || :primary_key, setting || :bigint]
49
+ end
50
+
51
+ # The type of api_keys.id, so that the foreign key matches it exactly.
52
+ def api_key_id_type
53
+ column = connection.columns(:api_keys).find { |candidate| candidate.name == "id" }
54
+ return primary_and_foreign_key_types.last unless column
55
+
56
+ sql_type = column.sql_type.to_s.downcase
57
+ if sql_type.include?("uuid")
58
+ :uuid
59
+ elsif column.type == :integer
60
+ sql_type.include?("bigint") || column.limit == 8 ? :bigint : :integer
61
+ else
62
+ column.type
63
+ end
64
+ end
65
+
66
+ # :jsonb on PostgreSQL, :json elsewhere, as on the api_keys table.
67
+ def json_column_type
68
+ connection.adapter_name.downcase.include?("postg") ? :jsonb : :json
69
+ end
70
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Migration to create the table of Idempotency-Keys (api_keys 0.7.0). Each
4
+ # row is one write under one API key's Idempotency-Key header
5
+ # (ApiKeys::Idempotency, ApiKeys::IdempotencyKey): the request it stands for
6
+ # as a digest, and, committed with the write itself, its status and a
7
+ # reference to the record the write made or changed. A row never keeps a
8
+ # response body, so a secret shown once is never stored.
9
+ #
10
+ # Only apps that use ApiKeys::Idempotency need this table.
11
+ class CreateApiKeysIdempotencyKeys < ActiveRecord::Migration<%= migration_version %>
12
+ def change
13
+ primary_key_type, foreign_key_type = primary_and_foreign_key_types
14
+
15
+ create_table :api_keys_idempotency_keys, id: primary_key_type do |t|
16
+ # Each API key has its own keys. A deleted API key takes them with it.
17
+ t.references :api_key, null: false, type: api_key_id_type, foreign_key: { on_delete: :cascade }, index: false
18
+ t.string :key, null: false
19
+ t.string :request_digest
20
+ t.integer :response_status
21
+ t.references :resource, polymorphic: true, null: true, type: foreign_key_type, index: false
22
+ t.timestamps
23
+ t.index :created_at
24
+ t.index [:api_key_id, :key], unique: true
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ # The key types the install generator gives the api_keys table: the app's
31
+ # `primary_key_type` generator setting, else Rails' defaults.
32
+ def primary_and_foreign_key_types
33
+ generators = Rails.application&.config&.generators
34
+ setting = generators&.options&.dig(generators.orm, :primary_key_type)
35
+ [setting || :primary_key, setting || :bigint]
36
+ end
37
+
38
+ # The type of api_keys.id, so that the foreign key matches it exactly.
39
+ def api_key_id_type
40
+ column = connection.columns(:api_keys).find { |candidate| candidate.name == "id" }
41
+ return primary_and_foreign_key_types.last unless column
42
+
43
+ sql_type = column.sql_type.to_s.downcase
44
+ if sql_type.include?("uuid")
45
+ :uuid
46
+ elsif column.type == :integer
47
+ sql_type.include?("bigint") || column.limit == 8 ? :bigint : :integer
48
+ else
49
+ column.type
50
+ end
51
+ end
52
+ end
@@ -26,6 +26,10 @@ class CreateApiKeysTable < ActiveRecord::Migration<%= migration_version %>
26
26
  # Who owns this key? Can be null for ownerless keys.
27
27
  t.references :owner, polymorphic: true, null: true, type: foreign_key_type
28
28
 
29
+ # Who created this key: the person behind it, for keys whose owner is
30
+ # not a person (an organization). Optional.
31
+ t.references :created_by, polymorphic: true, null: true, type: foreign_key_type
32
+
29
33
  # Optional list of permissions granted to this key (array of strings)
30
34
  t.send(json_column_type, :scopes, default: [], null: false)
31
35
 
@@ -327,6 +327,24 @@ ApiKeys.configure do |config|
327
327
  # when your network ingress rejects requests that bypass that vendor:
328
328
  # config.client_ip_resolver = ->(request) { request.headers.fetch("CF-Connecting-IP") }
329
329
 
330
+ # ============================================================================
331
+ # THE PERSON BEHIND A KEY
332
+ # ============================================================================
333
+ #
334
+ # When keys belong to an organization, name the person who creates each one:
335
+ #
336
+ # current_organization.create_api_key!(name: "CI", created_by: current_user)
337
+ #
338
+ # Then the check below can stop a key once that person no longer stands
339
+ # behind it (they left, or lost the right to manage keys). It runs after the
340
+ # scope check, on every authenticated request. A false or nil answer refuses
341
+ # the request with 403 `untrusted_key`. Keys made before you set created_by
342
+ # have none: decide what the check does with them.
343
+ #
344
+ # Existing installs: rails generate api_keys:add_created_by && rails db:migrate
345
+ # Default: nil (every valid key is trusted)
346
+ # config.trusted_key_check = ->(api_key) { api_key.owner.managed_by?(api_key.created_by) }
347
+
330
348
  # ============================================================================
331
349
  # BACKGROUND JOBS & CALLBACKS
332
350
  # ============================================================================
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.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - rameerez
@@ -106,17 +106,21 @@ files:
106
106
  - lib/api_keys/authentication.rb
107
107
  - lib/api_keys/configuration.rb
108
108
  - lib/api_keys/controller.rb
109
+ - lib/api_keys/device_flow.rb
109
110
  - lib/api_keys/engine.rb
110
111
  - lib/api_keys/errors.rb
111
112
  - lib/api_keys/form_builder_extensions.rb
112
113
  - lib/api_keys/helpers/expiration_options.rb
113
114
  - lib/api_keys/helpers/token_session.rb
114
115
  - lib/api_keys/helpers/view_helpers.rb
116
+ - lib/api_keys/idempotency.rb
115
117
  - lib/api_keys/jobs/callbacks_job.rb
116
118
  - lib/api_keys/jobs/update_stats_job.rb
117
119
  - lib/api_keys/logging.rb
118
120
  - lib/api_keys/models/api_key.rb
119
121
  - lib/api_keys/models/concerns/has_api_keys.rb
122
+ - lib/api_keys/models/device_authorization.rb
123
+ - lib/api_keys/models/idempotency_key.rb
120
124
  - lib/api_keys/restrictions.rb
121
125
  - lib/api_keys/services/authenticator.rb
122
126
  - lib/api_keys/services/digestor.rb
@@ -124,14 +128,20 @@ files:
124
128
  - lib/api_keys/tenant_resolution.rb
125
129
  - lib/api_keys/version.rb
126
130
  - lib/generators/api_keys/add_authentication_index_generator.rb
131
+ - lib/generators/api_keys/add_created_by_generator.rb
132
+ - lib/generators/api_keys/add_device_authorizations_generator.rb
133
+ - lib/generators/api_keys/add_idempotency_keys_generator.rb
127
134
  - lib/generators/api_keys/add_key_activity_generator.rb
128
135
  - lib/generators/api_keys/add_key_types_generator.rb
129
136
  - lib/generators/api_keys/add_restrictions_generator.rb
130
137
  - lib/generators/api_keys/install_generator.rb
131
138
  - lib/generators/api_keys/templates/add_authentication_index_to_api_keys.rb.erb
139
+ - lib/generators/api_keys/templates/add_created_by_to_api_keys.rb.erb
132
140
  - lib/generators/api_keys/templates/add_key_activity_to_api_keys.rb.erb
133
141
  - lib/generators/api_keys/templates/add_key_types_to_api_keys.rb.erb
134
142
  - lib/generators/api_keys/templates/add_restrictions_to_api_keys.rb.erb
143
+ - lib/generators/api_keys/templates/create_api_keys_device_authorizations.rb.erb
144
+ - lib/generators/api_keys/templates/create_api_keys_idempotency_keys.rb.erb
135
145
  - lib/generators/api_keys/templates/create_api_keys_table.rb.erb
136
146
  - lib/generators/api_keys/templates/initializer.rb
137
147
  homepage: https://github.com/rameerez/api_keys