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: 7d5beb5e03e8d5bc749e2a05456c2b0ac0105fb9ee7be6bb1c15e0a47800f759
4
- data.tar.gz: 1da205c1b54d157cf01ef194382eae9e69b8cb1ec920f1497ab791438ad30408
3
+ metadata.gz: aa734bb277bbb577d7e2ad46f6adc22c735c96d958ae85551385d61175153572
4
+ data.tar.gz: 8628007d90fc67798f388c2c8079ba1c98386ab43f764ac1c2948d141eaa5b85
5
5
  SHA512:
6
- metadata.gz: be22eed1e97b88042860f23968f3a6c84cd1ec8e60ff8547f097293d456d743fc3ad819c20696357ba3453b60fbcb3ff1e81f593160ce7b4e86e71e6777a3f8c
7
- data.tar.gz: 22061fc8c3fe051cb2104dfd3e19a85226469c85bdd0190b7f22291b8e44d4d0cfacbc34dbfe8168c1ce61983f1dcb229bdac2deb220826eac11acbfd23e789d
6
+ metadata.gz: a7ffa6b96cc5e9bd5c02eacae8a2b542422c1b5182d0b5886ffa53b9a44860e4c7ad543af8d6c6459498c885942a37138c1f6a1384d94bd7f710cd32b8c5c9de
7
+ data.tar.gz: 46ab93c5d3df04e62dcd1c130f5c4cc7867626ede3beae5545d409ab57252c5801064f335aa429744c01671e43d8ee86a9ecef97d26b20ede49812e5ac7f3204
data/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## [0.2.0] - 2025-06-03
2
+
3
+ - Make gem owner-agnostic: API keys can now belong to any model (User, Organization, Team, etc.)
4
+ - Add flexible dashboard configuration for custom owner models
5
+ - Add support for multi-tenant and team-based API key ownership
6
+ - Improve documentation with common ownership scenarios
7
+ - Add configuration options for current_owner_method and authenticate_owner_method
8
+
1
9
  ## [0.1.0] - 2025-04-30
2
10
 
3
11
  - Initial release
data/README.md CHANGED
@@ -1,8 +1,8 @@
1
- # 🔑 `api_keys` – Gate your Rails API with secure, self-serve API keys
1
+ # 🔑 `api_keys` – Secure API keys for your Rails app
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/pay.svg)](https://badge.fury.io/rb/pay)
3
+ [![Gem Version](https://badge.fury.io/rb/api_keys.svg)](https://badge.fury.io/rb/api_keys)
4
4
 
5
- `api_keys` makes it dead simple to add secure, production-ready API key authentication to your Rails app. Generate keys, restrict scopes, auto-expire tokens, revoke tokens. It also provides a self-serve dashboard for your users to self-issue and manage their API keys themselves. All tokens are hashed securely by default, and never stored in plaintext.
5
+ `api_keys` makes it simple to add secure, production-ready API key authentication to any Rails app. Generate keys, restrict scopes, auto-expire tokens, revoke tokens, gate endpoints. It also provides a self-serve dashboard for your users to self-issue and manage their API keys themselves. All tokens are hashed securely by default, and never stored in plaintext.
6
6
 
7
7
  [ 🟢 [Live interactive demo website](https://apikeys.rameerez.com) ]
8
8
 
@@ -50,9 +50,9 @@ end
50
50
 
51
51
  It'd work the same if you want your `Organization` or your `Project` records to have API keys.
52
52
 
53
- ### Mount the dashboard engine
53
+ ### Mount the dashboard engine so your users can self-serve API keys
54
54
 
55
- The goal of `api_keys` is to allow you to turn your Rails app into an API platform with secure key authentication in minutes, as in: drop in this gem and you're pretty much done.
55
+ The goal of `api_keys` is to allow you to turn your Rails app into an API platform with secure key authentication in minutes, as in: drop in this gem and you're pretty much done with API key management.
56
56
 
57
57
  To achieve that, the gem provides a ready-to-go dashboard you can just mount in your `routes.rb` like this:
58
58
 
@@ -60,7 +60,70 @@ To achieve that, the gem provides a ready-to-go dashboard you can just mount in
60
60
  mount ApiKeys::Engine => '/settings/api-keys'
61
61
  ```
62
62
 
63
- Now your users can:
63
+ #### Default behavior (User-owned keys)
64
+
65
+ By default, the dashboard expects:
66
+ - A `current_user` method that returns the currently logged-in user
67
+ - An `authenticate_user!` method that ensures a user is logged in
68
+
69
+ This works out-of-the-box with Devise and most authentication solutions where `User` owns the API keys.
70
+
71
+ #### Custom owner models (Organization, Team, etc.)
72
+
73
+ If your API keys belong to a different model (e.g., `Organization`), you **must** configure the dashboard in your initializer:
74
+
75
+ ```ruby
76
+ # config/initializers/api_keys.rb
77
+ ApiKeys.configure do |config|
78
+ # Tell the dashboard how to find the current API key owner
79
+ config.current_owner_method = :current_organization
80
+
81
+ # Tell the dashboard how to ensure the owner is authenticated
82
+ config.authenticate_owner_method = :authenticate_organization!
83
+ end
84
+ ```
85
+
86
+ These methods must exist in your `ApplicationController` (or wherever the engine is mounted). For example:
87
+
88
+ ```ruby
89
+ class ApplicationController < ActionController::Base
90
+ def current_organization
91
+ # Your logic to return the current organization
92
+ @current_organization ||= Organization.find(session[:organization_id])
93
+ end
94
+
95
+ def authenticate_organization!
96
+ redirect_to login_path unless current_organization
97
+ end
98
+ end
99
+ ```
100
+
101
+ #### Common scenarios
102
+
103
+ **Organization with user membership:**
104
+ ```ruby
105
+ # When organizations own keys but users manage them
106
+ config.current_owner_method = :current_organization
107
+ config.authenticate_owner_method = :require_organization_member!
108
+ ```
109
+
110
+ **Multi-tenant applications:**
111
+ ```ruby
112
+ # When each tenant/account owns keys
113
+ config.current_owner_method = :current_account
114
+ config.authenticate_owner_method = :authenticate_account!
115
+ ```
116
+
117
+ **Team-based ownership:**
118
+ ```ruby
119
+ # When teams own keys
120
+ config.current_owner_method = :current_team
121
+ config.authenticate_owner_method = :ensure_team_access!
122
+ ```
123
+
124
+ #### What the dashboard provides
125
+
126
+ Once configured, your users can:
64
127
  - self-issue new API keys
65
128
  - set expiration dates
66
129
  - attach scopes / permissions to individual keys
@@ -13,50 +13,44 @@ module ApiKeys
13
13
  # Include the main controller concern which bundles authentication and tenant resolution
14
14
  include ApiKeys::Controller
15
15
 
16
- # Ensure user is authenticated for all actions within this engine
17
- # IMPORTANT: This assumes the host application provides a `current_user` method
18
- # and potentially a `authenticate_user!` method (like Devise).
19
- # You might need to adjust this based on the host app's authentication system.
20
- before_action :authenticate_api_keys_user!
16
+ # Ensure the owner is authenticated for all actions within this engine
17
+ # This uses the configured authentication method (defaults to authenticate_user!)
18
+ before_action :authenticate_api_keys_owner!
21
19
 
22
20
  private
23
21
 
24
- # Placeholder method to authenticate the user accessing the engine.
25
- # Relies on the host application providing `authenticate_user!` and `current_user`.
26
- # Developers might need to override this in their application or configure
27
- # the authentication method if it differs from standard Devise/similar patterns.
28
- def authenticate_api_keys_user!
29
- # Try common authentication methods
30
- if defined?(authenticate_user!)
31
- authenticate_user!
32
- elsif defined?(require_login)
33
- require_login # Common in Sorcery
22
+ # Authenticates the owner accessing the engine.
23
+ # Uses the configured authentication method from ApiKeys.configuration
24
+ def authenticate_api_keys_owner!
25
+ auth_method = ApiKeys.configuration.authenticate_owner_method
26
+
27
+ # Try to call the configured authentication method
28
+ if auth_method && respond_to?(auth_method, true)
29
+ send(auth_method)
30
+ elsif auth_method && defined?(auth_method)
31
+ send(auth_method)
34
32
  else
35
- # Fallback or raise error if no known authentication method is found
36
- unless current_api_keys_user
37
- # Redirect or render error if no user context is available
38
- # Choose the appropriate action based on expected host app behavior
39
- redirect_to main_app.root_path, alert: "You need to sign in or sign up before continuing." rescue render plain: "Unauthorized", status: :unauthorized
33
+ # Fallback: check if owner is present
34
+ unless current_api_keys_owner
35
+ redirect_to main_app.root_path, alert: "You need to sign in before continuing." rescue render plain: "Unauthorized", status: :unauthorized
40
36
  end
41
37
  end
42
38
  end
43
39
 
44
- # Helper method to access the current user from the host application.
45
- # Assumes the host app provides `current_user`.
46
- def current_api_keys_user
47
- # Use `super` if the parent controller defines `current_user`
48
- # Otherwise, try calling `current_user` directly on self if it's mixed in.
49
- if defined?(super)
50
- super
51
- elsif defined?(current_user)
52
- current_user
40
+ # Helper method to access the current owner from the host application.
41
+ # Uses the configured method name (defaults to :current_user)
42
+ def current_api_keys_owner
43
+ owner_method = ApiKeys.configuration.current_owner_method
44
+
45
+ if owner_method && respond_to?(owner_method, true)
46
+ send(owner_method)
53
47
  else
54
- nil # No user context found
48
+ nil # No owner context found
55
49
  end
56
50
  end
57
51
 
58
- # Expose current_api_keys_user as a helper method for views
59
- helper_method :current_api_keys_user
52
+ # Expose current_api_keys_owner as a helper method for views
53
+ helper_method :current_api_keys_owner
60
54
 
61
55
  end
62
56
  end
@@ -1,16 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ApiKeys
4
- # Controller for managing API keys belonging to the current user.
4
+ # Controller for managing API keys belonging to the current owner.
5
5
  class KeysController < ApplicationController
6
6
  before_action :set_api_key, only: [:show, :edit, :update, :revoke]
7
7
 
8
8
  # GET /keys
9
9
  def index
10
10
  # Fetch only active keys for the main list, maybe sorted by creation date
11
- @api_keys = current_api_keys_user.api_keys.active.order(created_at: :desc)
11
+ @api_keys = current_api_keys_owner.api_keys.active.order(created_at: :desc)
12
12
  # Optionally, fetch inactive ones for a separate section or filter
13
- @inactive_api_keys = current_api_keys_user.api_keys.inactive.order(created_at: :desc)
13
+ @inactive_api_keys = current_api_keys_owner.api_keys.inactive.order(created_at: :desc)
14
14
  end
15
15
 
16
16
  # GET /keys/:id
@@ -30,7 +30,7 @@ module ApiKeys
30
30
 
31
31
  # GET /keys/new
32
32
  def new
33
- @api_key = current_api_keys_user.api_keys.build
33
+ @api_key = current_api_keys_owner.api_keys.build
34
34
  end
35
35
 
36
36
  # POST /keys
@@ -38,7 +38,7 @@ module ApiKeys
38
38
  # Use the HasApiKeys helper method to create the key
39
39
  begin
40
40
  # create_api_key! now returns the ApiKey instance
41
- @api_key = current_api_keys_user.create_api_key!(
41
+ @api_key = current_api_keys_owner.create_api_key!(
42
42
  name: api_key_params[:name],
43
43
  scopes: api_key_params[:scopes],
44
44
  expires_at: parse_expiration(api_key_params[:expires_at_preset])
@@ -59,7 +59,7 @@ module ApiKeys
59
59
  render :new, status: :unprocessable_entity
60
60
  rescue => e # Catch other potential errors
61
61
  flash.now[:alert] = "An unexpected error occurred: #{e.message}"
62
- @api_key = current_api_keys_user.api_keys.build(api_key_params) # Rebuild form
62
+ @api_key = current_api_keys_owner.api_keys.build(api_key_params) # Rebuild form
63
63
  render :new, status: :unprocessable_entity
64
64
  end
65
65
  end
@@ -93,7 +93,7 @@ module ApiKeys
93
93
 
94
94
  # Use callbacks to share common setup or constraints between actions.
95
95
  def set_api_key
96
- @api_key = current_api_keys_user.api_keys.find(params[:id])
96
+ @api_key = current_api_keys_owner.api_keys.find(params[:id])
97
97
  rescue ActiveRecord::RecordNotFound
98
98
  redirect_to keys_path, alert: "API key not found."
99
99
  end
@@ -38,7 +38,7 @@
38
38
  <%# containing an array of scope strings allowed for this context. %>
39
39
  <%# This list might come from global configuration or owner-specific settings. %>
40
40
  <%# Example: @available_scopes = ["read", "write", "admin"] %>
41
- <% @available_scopes = current_api_keys_user.class.api_keys_settings.dig(:default_scopes) %>
41
+ <% @available_scopes = current_api_keys_owner.class.api_keys_settings.dig(:default_scopes) %>
42
42
  <% if defined?(@available_scopes) && @available_scopes.present? %>
43
43
  <div>
44
44
  <%= form.label :scopes, "Permissions" %>
@@ -70,7 +70,7 @@
70
70
  <%# Define available scopes for the edit form context %>
71
71
  <%# This assumes the available scopes are the same as the default ones. %>
72
72
  <%# Consider passing @available_scopes from the controller if logic is more complex. %>
73
- <% @available_scopes = current_api_keys_user.class.api_keys_settings.dig(:default_scopes) %>
73
+ <% @available_scopes = current_api_keys_owner.class.api_keys_settings.dig(:default_scopes) %>
74
74
  <% if defined?(@available_scopes) && @available_scopes.present? %>
75
75
  <div>
76
76
  <%= form.label :scopes, "Permissions / Scopes" %>
@@ -24,6 +24,9 @@ module ApiKeys
24
24
  # Engine Configuration
25
25
  attr_accessor :parent_controller
26
26
 
27
+ # Owner Context Configuration
28
+ attr_accessor :current_owner_method, :authenticate_owner_method
29
+
27
30
  # Optional Behaviors
28
31
  attr_accessor :default_max_keys_per_owner, :require_key_name
29
32
  attr_accessor :expire_after, :default_scopes, :track_requests_count
@@ -84,6 +87,10 @@ module ApiKeys
84
87
  # Engine Configuration
85
88
  @parent_controller = '::ApplicationController'
86
89
 
90
+ # Owner Context Configuration
91
+ @current_owner_method = :current_user # Default to current_user for backward compatibility
92
+ @authenticate_owner_method = :authenticate_user! # Default to authenticate_user! for Devise compatibility
93
+
87
94
  # Optional Behaviors
88
95
  @default_max_keys_per_owner = nil # No global key limit per owner
89
96
  @require_key_name = false # Don't require names for keys globally
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ApiKeys
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/api_keys.rb CHANGED
@@ -2,22 +2,15 @@
2
2
 
3
3
  # This is the entry point to the gem.
4
4
 
5
+ # Global requires that don't depend on ApiKeys module structure
5
6
  require "rails"
6
7
  require "active_record"
7
- require "active_support/all"
8
+ require "active_support/all" # For ActiveSupport::SecurityUtils, etc. used in Configuration
8
9
 
9
- require "api_keys/version"
10
- require "api_keys/configuration"
11
-
12
- require "api_keys/models/concerns/has_api_keys"
13
-
14
- require "api_keys/models/api_key"
15
-
16
- # Rails integration
17
- require "api_keys/engine" if defined?(Rails)
18
-
19
- # Main module that serves as the primary interface to the gem.
20
- # Most methods here delegate to Configuration, which is the single source of truth for all config in the initializer
10
+ # --------- Core ApiKeys Module Definition ---------
11
+ # Define the ApiKeys module itself and its primary interface methods early.
12
+ # This ensures that ApiKeys.configuration and ApiKeys.configure are available
13
+ # when other gem files are loaded and potentially use them at load time.
21
14
  module ApiKeys
22
15
  # Custom error classes
23
16
  class Error < StandardError; end
@@ -25,24 +18,56 @@ module ApiKeys
25
18
  class << self
26
19
  attr_writer :configuration
27
20
 
21
+ # Provides access to the gem's configuration.
22
+ # Initializes with default settings if not already configured.
23
+ # Note: ApiKeys::Configuration class must be loaded before this method
24
+ # is called in a way that triggers instantiation (e.g. first call).
28
25
  def configuration
29
- @configuration ||= Configuration.new
26
+ # Ensure @configuration is initialized with an instance of ApiKeys::Configuration.
27
+ # The ApiKeys::Configuration class itself is defined in 'api_keys/configuration.rb'.
28
+ @configuration ||= ::ApiKeys::Configuration.new
30
29
  end
31
30
 
32
- # Configure the gem with a block (main entry point)
31
+ # Main configuration block for the gem.
32
+ # Example:
33
+ # ApiKeys.configure do |config|
34
+ # config.token_prefix = "my_app_"
35
+ # end
33
36
  def configure
34
37
  yield(configuration)
35
38
  end
36
39
 
37
40
  # Resets the configuration to its default values.
38
- # Useful for testing.
41
+ # Useful primarily for testing environments.
39
42
  def reset_configuration!
40
- @configuration = Configuration.new
43
+ @configuration = ::ApiKeys::Configuration.new
41
44
  end
42
-
43
45
  end
44
46
  end
45
47
 
48
+ # --------- Gem Component Requires ---------
49
+ # Order is important here.
50
+ # 1. Version: Typically first.
51
+ # 2. Configuration class: Needed by ApiKeys.configuration method.
52
+ # 3. Other components: Controllers, models, engine, etc., which might use the configuration.
53
+
54
+ require "api_keys/version"
55
+ require "api_keys/configuration" # Defines the ApiKeys::Configuration class
56
+
57
+ # Files that might depend on ApiKeys.configuration being available
58
+ require "api_keys/controller" # This can lead to loading jobs, etc.
59
+ require "api_keys/models/concerns/has_api_keys"
60
+ require "api_keys/models/api_key"
61
+
62
+ # Rails integration (Engine)
63
+ # The Engine might also access ApiKeys.configuration during its initialization.
64
+ require "api_keys/engine" if defined?(Rails)
65
+
66
+ # Consider if a Railtie is needed and where its require should go.
67
+ # If it also needs ApiKeys.configuration, it should be after the main module definition
68
+ # and after api_keys/configuration.
69
+ # require "api_keys/railtie" if defined?(Rails::Railtie)
70
+
46
71
  # TODO: Require other necessary files like configuration, engine, etc.
47
72
  # require_relative "api_keys/configuration"
48
73
  # require_relative "api_keys/engine"
@@ -46,8 +46,19 @@ module ApiKeys
46
46
  say " end"
47
47
  say " # ..."
48
48
  say " end"
49
- say "\n 3. Optionally, configure the gem behavior in `config/initializers/api_keys.rb` if needed."
50
- say "\n 4. In your app's API controllers, verify API keys by including `ApiKeys::Controller`, and adding the before_action to the desired endpoints:"
49
+ say "\n 3. IMPORTANT: If API keys belong to a model other than User (e.g., Organization),"
50
+ say " configure the owner context in `config/initializers/api_keys.rb`:", :yellow
51
+ say " # For Organization-owned API keys:"
52
+ say " config.current_owner_method = :current_organization"
53
+ say " config.authenticate_owner_method = :authenticate_organization!"
54
+ say "\n The dashboard requires these methods to exist in your ApplicationController"
55
+ say " or wherever you mount the engine. They should:"
56
+ say " - `current_owner_method`: return the logged-in owner (e.g., current_organization)"
57
+ say " - `authenticate_owner_method`: ensure the owner is authenticated"
58
+ say "\n 4. Mount the API keys dashboard in your `routes.rb` to provide a self-serve interface:"
59
+ say " # In config/routes.rb"
60
+ say " mount ApiKeys::Engine => '/settings/api-keys'"
61
+ say "\n 5. In your app's API controllers, verify API keys by including `ApiKeys::Controller`:"
51
62
  say " # Example for app/controllers/api/base_controller.rb"
52
63
  say " class Api::BaseController < ActionController::API"
53
64
  say " include ApiKeys::Controller"
@@ -38,6 +38,30 @@ ApiKeys.configure do |config|
38
38
  # Default: :sha256
39
39
  # config.hash_strategy = :bcrypt
40
40
 
41
+ # === Dashboard Configuration ===
42
+
43
+ # IMPORTANT: Owner Context Configuration
44
+ # The api_keys dashboard needs to know how to find the current owner
45
+ # of API keys in your application. By default, it assumes you have
46
+ # a User model with current_user/authenticate_user! methods (Devise-style).
47
+ #
48
+ # If your API keys belong to a different model (e.g., Organization),
49
+ # you MUST configure these methods:
50
+
51
+ # The method that returns the current API key owner in your controllers.
52
+ # This should return the object that has_api_keys (e.g., current_organization).
53
+ # Default: :current_user
54
+ # config.current_owner_method = :current_organization
55
+
56
+ # The method that authenticates/requires login for the dashboard.
57
+ # This should ensure the owner is logged in before accessing the dashboard.
58
+ # Default: :authenticate_user!
59
+ # config.authenticate_owner_method = :authenticate_organization!
60
+
61
+ # Example for Organization-owned keys:
62
+ # config.current_owner_method = :current_organization
63
+ # config.authenticate_owner_method = :authenticate_organization!
64
+
41
65
  # === Optional Behaviors ===
42
66
 
43
67
  # Global limit on the number of *active* keys an owner can have.
@@ -70,7 +94,7 @@ ApiKeys.configure do |config|
70
94
  # Set to 0 or nil to disable caching.
71
95
  # Uses Rails.cache.
72
96
  # Default: 5.seconds
73
- # config.cache_ttl = 30.seconds # Higher TTL = higher risk of revoked-but-still-valid edge cases
97
+ # config.cache_ttl = 30.seconds # Higher TTL = higher risk of "revoked-but-still-valid" edge cases
74
98
 
75
99
  # === Security ===
76
100
 
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: 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
  - rameerez
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-04-30 00:00:00.000000000 Z
10
+ date: 2025-06-03 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: rails