api_keys 0.6.0 ā 0.8.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 +4 -4
- data/CHANGELOG.md +38 -0
- data/README.md +287 -2
- data/lib/api_keys/authentication.rb +51 -0
- data/lib/api_keys/configuration.rb +65 -2
- data/lib/api_keys/device_flow.rb +146 -0
- data/lib/api_keys/engine.rb +9 -0
- data/lib/api_keys/errors.rb +11 -0
- data/lib/api_keys/helpers/view_helpers.rb +2 -1
- data/lib/api_keys/idempotency.rb +155 -0
- data/lib/api_keys/models/api_key.rb +30 -3
- data/lib/api_keys/models/concerns/has_api_keys.rb +9 -1
- data/lib/api_keys/models/device_authorization.rb +333 -0
- data/lib/api_keys/models/idempotency_key.rb +184 -0
- data/lib/api_keys/services/authenticator.rb +10 -3
- data/lib/api_keys/version.rb +1 -1
- data/lib/api_keys.rb +6 -0
- data/lib/generators/api_keys/add_created_by_generator.rb +50 -0
- data/lib/generators/api_keys/add_device_authorizations_generator.rb +51 -0
- data/lib/generators/api_keys/add_idempotency_keys_generator.rb +52 -0
- data/lib/generators/api_keys/templates/add_created_by_to_api_keys.rb.erb +33 -0
- data/lib/generators/api_keys/templates/create_api_keys_device_authorizations.rb.erb +70 -0
- data/lib/generators/api_keys/templates/create_api_keys_idempotency_keys.rb.erb +52 -0
- data/lib/generators/api_keys/templates/create_api_keys_table.rb.erb +4 -0
- data/lib/generators/api_keys/templates/initializer.rb +23 -0
- metadata +11 -1
|
@@ -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
|
|
|
@@ -9,6 +9,11 @@ ApiKeys.configure do |config|
|
|
|
9
9
|
# Default: "Authorization" (expects "Bearer <token>")
|
|
10
10
|
# config.header = "Authorization"
|
|
11
11
|
|
|
12
|
+
# Accept the header only as "Bearer <token>", refusing a bare token with
|
|
13
|
+
# 401 invalid_token. Recommended for public APIs.
|
|
14
|
+
# Default: false (a bare token is accepted too)
|
|
15
|
+
# config.require_bearer_scheme = true
|
|
16
|
+
|
|
12
17
|
# The query parameter name to check as a fallback if the header is missing.
|
|
13
18
|
# WARNING: Query parameters appear in logs, browser history, and Referer headers.
|
|
14
19
|
# Only enable for development/testing. Set to nil to disable (recommended).
|
|
@@ -327,6 +332,24 @@ ApiKeys.configure do |config|
|
|
|
327
332
|
# when your network ingress rejects requests that bypass that vendor:
|
|
328
333
|
# config.client_ip_resolver = ->(request) { request.headers.fetch("CF-Connecting-IP") }
|
|
329
334
|
|
|
335
|
+
# ============================================================================
|
|
336
|
+
# THE PERSON BEHIND A KEY
|
|
337
|
+
# ============================================================================
|
|
338
|
+
#
|
|
339
|
+
# When keys belong to an organization, name the person who creates each one:
|
|
340
|
+
#
|
|
341
|
+
# current_organization.create_api_key!(name: "CI", created_by: current_user)
|
|
342
|
+
#
|
|
343
|
+
# Then the check below can stop a key once that person no longer stands
|
|
344
|
+
# behind it (they left, or lost the right to manage keys). It runs after the
|
|
345
|
+
# scope check, on every authenticated request. A false or nil answer refuses
|
|
346
|
+
# the request with 403 `untrusted_key`. Keys made before you set created_by
|
|
347
|
+
# have none: decide what the check does with them.
|
|
348
|
+
#
|
|
349
|
+
# Existing installs: rails generate api_keys:add_created_by && rails db:migrate
|
|
350
|
+
# Default: nil (every valid key is trusted)
|
|
351
|
+
# config.trusted_key_check = ->(api_key) { api_key.owner.managed_by?(api_key.created_by) }
|
|
352
|
+
|
|
330
353
|
# ============================================================================
|
|
331
354
|
# BACKGROUND JOBS & CALLBACKS
|
|
332
355
|
# ============================================================================
|
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
|
+
version: 0.8.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
|