rails_api_keys 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f6a89bf64215fd1c3fcad121b300e716eeff78787be8f438e63dca2c399b6bf7
4
- data.tar.gz: 2d4ec14c89eb7e8675d0036ff18d879532b74b93ff1311320b0d5f5a75531a50
3
+ metadata.gz: 5efd1a62dc4d875f9411e190b6cdeb33129d09c85790ea2f2c61cf12381b8123
4
+ data.tar.gz: 0bd1d07535c449f62b8a59517057a6f990b2524d57e4c5d6ab457e4b0636a850
5
5
  SHA512:
6
- metadata.gz: 9552c48d8976f5b1df454460273d9616a9203d2bb9a6987b643981b99d0ac3b93b2cd21af5c948afdaf4b414a48ec091cc2eb8f3699b77240a371a42f16c02b7
7
- data.tar.gz: f245f531718d6e288d1d108de0aee915f0f18a78dcbd743c0f44a1e7a86511446856719ef15b38370f7545c5e30104acbb7eab45fa65b4c4048d479f533f7ecf
6
+ metadata.gz: 2a106e93e5e874e58754bc2502845a6ef8f19ba78cba31806cbda95c52ab6721fc6600b78ad90c678c95d212675b070f0e1dbc4902f222dd2ee1d5c65d345872
7
+ data.tar.gz: 2d6ece664815bd002f8543789317e36f6dbe52490087fdd8a31d072ab1440eee975d1df94e7f8b209beab92d18515d30085a7426c460b66b1256dc24383c5047
data/CHANGELOG.md CHANGED
@@ -3,10 +3,26 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.0] - 2026-08-10
11
+
12
+ ### Added
13
+
14
+ - `has_api_keys` — Active Record macro for owner models (`User`, `Company`, etc.); registers allowed polymorphic owners
15
+ - `create_api_key!` — instance helper on owners that call `has_api_keys` (wraps `ApiKey.generate_for!`)
16
+
17
+ ### Changed
18
+
19
+ - `authenticate_api_key!` now enforces permissions from the HTTP method: GET/HEAD require read; other methods require write
20
+
21
+ ### Removed
22
+
23
+ - `require_api_permission!` — hosts no longer call this separately
24
+ - `config.owner_class` — replaced by opt-in `has_api_keys` on each owner model
25
+
10
26
  ## [0.1.0] - 2026-08-10
11
27
 
12
28
  ### Added
@@ -16,5 +32,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
16
32
  - `RailsApiKeys.configure` — optional `owner_class`, `token_prefix`, and `owner_active`
17
33
  - Install generator (`rails_api_keys:install`) for migration and initializer
18
34
 
19
- [Unreleased]: https://github.com/rubyroidlabs/rails_api_keys/compare/v0.1.0...HEAD
35
+ [Unreleased]: https://github.com/rubyroidlabs/rails_api_keys/compare/v0.2.0...HEAD
36
+ [0.2.0]: https://github.com/rubyroidlabs/rails_api_keys/compare/v0.1.0...v0.2.0
20
37
  [0.1.0]: https://github.com/rubyroidlabs/rails_api_keys/releases/tag/v0.1.0
data/README.md CHANGED
@@ -9,6 +9,7 @@ Host apps own UI, routes, and domain APIs. This gem stays thin on purpose.
9
9
  ## What this gem includes
10
10
 
11
11
  - `RailsApiKeys::ApiKey` — issue, authenticate, revoke
12
+ - `has_api_keys` — opt-in owner macro with `create_api_key!` (supports multiple models)
12
13
  - `RailsApiKeys::Authentication` — controller concern for Bearer tokens
13
14
  - Install generator (migration + initializer)
14
15
 
@@ -29,32 +30,28 @@ bin/rails generate rails_api_keys:install
29
30
  bin/rails db:migrate
30
31
  ```
31
32
 
32
- Until the gem is published on RubyGems, you can use git or a local path:
33
-
34
- ```ruby
35
- gem "rails_api_keys", git: "https://github.com/rubyroidlabs/rails_api_keys.git"
36
- # or: gem "rails_api_keys", path: "../rails_api_keys"
37
- ```
38
-
39
33
  ## Host setup
40
34
 
41
- On the owner model (defaults to `User`):
35
+ Opt in on each owner model with `has_api_keys` (any number of models):
42
36
 
43
37
  ```ruby
44
38
  class User < ApplicationRecord
45
- has_many :api_keys, as: :owner, class_name: "RailsApiKeys::ApiKey", dependent: :destroy
39
+ has_api_keys
40
+ end
41
+
42
+ class Company < ApplicationRecord
43
+ has_api_keys
46
44
  end
47
45
  ```
48
46
 
49
- Build your own controllers/UI to create keys (show the raw token once), list them, and revoke.
47
+ That declares `has_many :api_keys` and allows the model to own keys. Build your own controllers/UI to create keys (show the raw token once), list them, and revoke.
50
48
 
51
49
  ## Configuration
52
50
 
53
- Defaults suit a typical Devise `User` host:
51
+ Defaults suit a typical Devise `User` host; multiple owners are supported via `has_api_keys`:
54
52
 
55
53
  | Option | Default | Purpose |
56
54
  | --- | --- | --- |
57
- | `owner_class` | `"User"` | Expected owner class name |
58
55
  | `token_prefix` | `"#{AppName.downcase}_ak_"` | Prefix on generated raw tokens |
59
56
  | `owner_active` | `active_for_authentication?` when present | Reject keys whose owner is inactive |
60
57
 
@@ -64,7 +61,6 @@ Override only what you need:
64
61
  # config/initializers/rails_api_keys.rb
65
62
  RailsApiKeys.configure do |config|
66
63
  # config.token_prefix = "myapp_ak_"
67
- # config.owner_class = "Admin"
68
64
  # config.owner_active = ->(owner) { owner.active? }
69
65
  end
70
66
  ```
@@ -72,11 +68,7 @@ end
72
68
  ## Usage
73
69
 
74
70
  ```ruby
75
- key, raw = RailsApiKeys::ApiKey.generate_for!(
76
- owner: current_user,
77
- name: "Zapier",
78
- permission: :read
79
- )
71
+ key, raw = current_user.create_api_key!(name: "Zapier", permission: :read)
80
72
  # Show `raw` once — it cannot be recovered later.
81
73
 
82
74
  RailsApiKeys::ApiKey.authenticate(raw) # => key or nil
@@ -88,7 +80,6 @@ class Api::V1::BaseController < ActionController::API
88
80
  include RailsApiKeys::Authentication
89
81
 
90
82
  before_action :authenticate_api_key!
91
- before_action -> { require_api_permission!(:read) }
92
83
 
93
84
  # current_api_key / current_api_owner are available after authenticate
94
85
  end
@@ -100,18 +91,20 @@ Clients send:
100
91
  Authorization: Bearer <raw_token>
101
92
  ```
102
93
 
94
+ `authenticate_api_key!` rejects missing/invalid tokens with 401, then enforces permission from the HTTP method: GET/HEAD require read; other methods require write (403 if the key lacks it).
95
+
103
96
  ## Permissions
104
97
 
105
- | Permission | `allows_read?` | `allows_write?` |
98
+ | Permission | GET/HEAD | Other methods |
106
99
  | --- | --- | --- |
107
- | `read` | yes | no |
108
- | `read_write` | yes | yes |
100
+ | `read` | allowed | forbidden |
101
+ | `read_write` | allowed | allowed |
109
102
 
110
103
  Permissions are immutable after create. Revoke with `revoke!` (sets `revoked_at`).
111
104
 
112
105
  ## Security notes
113
106
 
114
- - Raw tokens are returned only from `generate_for!` and never stored
107
+ - Raw tokens are returned only from `create_api_key!` / `generate_for!` and never stored
115
108
  - Digests use SHA-256 (`token_digest`); UI can show `token_display_prefix`
116
109
  - Soft revoke via `revoked_at`; authentication ignores revoked keys
117
110
 
@@ -21,7 +21,7 @@ module RailsApiKeys
21
21
 
22
22
  validates :name, :token_digest, :token_display_prefix, :permission, presence: true
23
23
  validates :token_digest, uniqueness: true
24
- validate :owner_matches_configured_class
24
+ validate :owner_declares_has_api_keys
25
25
  validate :permission_immutable, on: :update
26
26
 
27
27
  scope :active, -> { where(revoked_at: nil) }
@@ -92,13 +92,11 @@ module RailsApiKeys
92
92
 
93
93
  private
94
94
 
95
- def owner_matches_configured_class
95
+ def owner_declares_has_api_keys
96
96
  return if owner.blank?
97
+ return if RailsApiKeys.owner_class?(owner.class)
97
98
 
98
- expected = RailsApiKeys.configuration.owner_class_name
99
- return if owner.class.name == expected
100
-
101
- errors.add(:owner, "must be a #{expected}")
99
+ errors.add(:owner, "must declare has_api_keys")
102
100
  end
103
101
 
104
102
  def permission_immutable
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Defaults: owner_class "User", token_prefix "<AppName>_ak_",
3
+ # Defaults: token_prefix "<AppName>_ak_",
4
4
  # owner_active via active_for_authentication? when available.
5
+ # Owner models opt in with +has_api_keys+.
5
6
  # Uncomment to override:
6
7
  #
7
8
  # RailsApiKeys.configure do |config|
@@ -5,9 +5,10 @@ require "active_support/concern"
5
5
  module RailsApiKeys
6
6
  # Controller concern for +ActionController::API+.
7
7
  #
8
- # Include and call {#authenticate_api_key!} / {#require_api_permission!} from
9
- # +before_action+. After a successful authenticate, +current_api_key+ and
10
- # +current_api_owner+ are set.
8
+ # Include and call {#authenticate_api_key!} from +before_action+. After a
9
+ # successful authenticate, +current_api_key+ and +current_api_owner+ are set.
10
+ # Permission is enforced from the HTTP method: GET/HEAD require read; other
11
+ # methods require write.
11
12
  #
12
13
  # Expects +Authorization: Bearer <raw_token>+.
13
14
  module Authentication
@@ -19,7 +20,8 @@ module RailsApiKeys
19
20
 
20
21
  private
21
22
 
22
- # Authenticates the Bearer token. Renders 401 and returns +false+ on failure.
23
+ # Authenticates the Bearer token and enforces method-based permission.
24
+ # Renders 401 on auth failure, 403 when the key lacks the required level.
23
25
  def authenticate_api_key!
24
26
  token = bearer_token
25
27
  key = RailsApiKeys::ApiKey.authenticate(token)
@@ -31,16 +33,15 @@ module RailsApiKeys
31
33
 
32
34
  @current_api_key = key
33
35
  @current_api_owner = key.owner
34
- true
36
+ enforce_api_permission!
35
37
  end
36
38
 
37
- # Requires +:read+ or +:write+. +read_write+ keys satisfy both. Renders 403 on failure.
38
- def require_api_permission!(level)
39
+ def enforce_api_permission!
39
40
  allowed =
40
- case level.to_sym
41
- when :read then current_api_key&.allows_read?
42
- when :write then current_api_key&.allows_write?
43
- else false
41
+ if request.get? || request.head?
42
+ current_api_key.allows_read?
43
+ else
44
+ current_api_key.allows_write?
44
45
  end
45
46
 
46
47
  return true if allowed
@@ -3,14 +3,13 @@
3
3
  module RailsApiKeys
4
4
  # Optional host overrides. Set via {RailsApiKeys.configure}.
5
5
  #
6
- # Defaults: +owner_class+ is +"User"+, +token_prefix+ derives from the app
7
- # name, and +owner_active+ uses Devise-style +active_for_authentication?+
8
- # when available.
6
+ # Defaults: +token_prefix+ derives from the app name, and +owner_active+ uses
7
+ # Devise-style +active_for_authentication?+ when available.
8
+ # Owner models opt in with {Owner.has_api_keys}.
9
9
  class Configuration
10
- attr_accessor :owner_class, :token_prefix, :owner_active
10
+ attr_accessor :token_prefix, :owner_active
11
11
 
12
12
  def initialize
13
- @owner_class = "User"
14
13
  @token_prefix = nil
15
14
  @owner_active = ->(owner) {
16
15
  if owner.respond_to?(:active_for_authentication?)
@@ -27,10 +26,6 @@ module RailsApiKeys
27
26
  "#{Rails.application.class.module_parent_name.downcase}_ak_"
28
27
  end
29
28
 
30
- def owner_class_name
31
- owner_class.to_s
32
- end
33
-
34
29
  def owner_active?(owner)
35
30
  owner_active.call(owner)
36
31
  end
@@ -3,5 +3,11 @@
3
3
  module RailsApiKeys
4
4
  class Engine < ::Rails::Engine
5
5
  isolate_namespace RailsApiKeys
6
+
7
+ initializer "rails_api_keys.active_record" do
8
+ ActiveSupport.on_load(:active_record) do
9
+ include RailsApiKeys::Owner
10
+ end
11
+ end
6
12
  end
7
13
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsApiKeys
4
+ # Mixes {.has_api_keys} into Active Record so host models can opt in as key owners.
5
+ module Owner
6
+ extend ActiveSupport::Concern
7
+
8
+ class_methods do
9
+ # Declares a polymorphic +has_many :api_keys+ and registers this class as an
10
+ # allowed owner for {RailsApiKeys::ApiKey}.
11
+ def has_api_keys(dependent: :destroy)
12
+ has_many :api_keys,
13
+ as: :owner,
14
+ class_name: "RailsApiKeys::ApiKey",
15
+ dependent: dependent
16
+
17
+ RailsApiKeys.register_owner_class!(self)
18
+
19
+ # Creates a key for this owner and returns +[record, raw_token]+.
20
+ define_method(:create_api_key!) do |name:, permission:|
21
+ RailsApiKeys::ApiKey.generate_for!(owner: self, name: name, permission: permission)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsApiKeys
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "set"
3
4
  require "rails_api_keys/version"
4
5
  require "rails_api_keys/configuration"
5
6
  require "rails_api_keys/authentication"
7
+ require "rails_api_keys/owner"
6
8
  require "rails_api_keys/engine"
7
9
 
8
10
  # Thin Rails engine for personal API key authentication.
@@ -21,8 +23,25 @@ module RailsApiKeys
21
23
  end
22
24
 
23
25
  # Resets configuration to defaults (primarily for tests).
26
+ # Does not clear {.owner_class_names}; those are registered at model load via {Owner#has_api_keys}.
24
27
  def reset_configuration!
25
28
  @configuration = Configuration.new
26
29
  end
30
+
31
+ # Records +klass+ as an allowed API key owner (idempotent).
32
+ def register_owner_class!(klass)
33
+ owner_class_names << klass.name
34
+ end
35
+
36
+ # @return [Boolean] whether +klass+ (or its name) called {Owner#has_api_keys}
37
+ def owner_class?(klass)
38
+ name = klass.is_a?(Module) ? klass.name : klass.to_s
39
+ owner_class_names.include?(name)
40
+ end
41
+
42
+ # @return [Set<String>] registered owner class names
43
+ def owner_class_names
44
+ @owner_class_names ||= Set.new
45
+ end
27
46
  end
28
47
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_api_keys
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pavel Pershko
@@ -50,16 +50,17 @@ files:
50
50
  - lib/rails_api_keys/authentication.rb
51
51
  - lib/rails_api_keys/configuration.rb
52
52
  - lib/rails_api_keys/engine.rb
53
+ - lib/rails_api_keys/owner.rb
53
54
  - lib/rails_api_keys/version.rb
54
55
  homepage: https://github.com/rubyroidlabs/rails_api_keys
55
56
  licenses:
56
57
  - MIT
57
58
  metadata:
58
59
  homepage_uri: https://github.com/rubyroidlabs/rails_api_keys
59
- source_code_uri: https://github.com/rubyroidlabs/rails_api_keys/tree/v0.1.0
60
- changelog_uri: https://github.com/rubyroidlabs/rails_api_keys/blob/v0.1.0/CHANGELOG.md
60
+ source_code_uri: https://github.com/rubyroidlabs/rails_api_keys/tree/v0.2.0
61
+ changelog_uri: https://github.com/rubyroidlabs/rails_api_keys/blob/v0.2.0/CHANGELOG.md
61
62
  bug_tracker_uri: https://github.com/rubyroidlabs/rails_api_keys/issues
62
- documentation_uri: https://www.rubydoc.info/gems/rails_api_keys/0.1.0
63
+ documentation_uri: https://www.rubydoc.info/gems/rails_api_keys/0.2.0
63
64
  rubygems_mfa_required: 'true'
64
65
  allowed_push_host: https://rubygems.org
65
66
  rdoc_options: []