Skip to content

Commit c120b1c

Browse files
cursoragentjaracursorsh
andcommitted
Change default digest to SHA256, update token generation, add tenant resolution tests
Co-authored-by: jaracursorsh <[email protected]>
1 parent 665cff0 commit c120b1c

File tree

6 files changed

+127
-14
lines changed

6 files changed

+127
-14
lines changed

lib/api_keys/tenant_resolution.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ def current_api_tenant
3333
@current_api_tenant = resolver&.call(current_api_key)
3434
rescue StandardError => e
3535
# Log error but don't break the request if resolver fails
36-
Rails.logger.error "[ApiKeys] Tenant resolution failed: #{e.message}" if defined?(Rails.logger)
36+
if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
37+
Rails.logger.error "[ApiKeys] Tenant resolution failed: #{e.message}"
38+
end
3739
@current_api_tenant = nil
3840
end
3941
end

test/controllers/authentication_test.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,5 +133,28 @@ def clear_enqueued_jobs
133133
refute_includes job_classes, ApiKeys::Jobs::CallbacksJob
134134
refute_includes job_classes, ApiKeys::Jobs::UpdateStatsJob
135135
end
136+
137+
test "authenticate_api_key! with multiple required scopes succeeds only if all present" do
138+
user = User.create!(name: "Multi Scope User")
139+
key = ApiKeys::ApiKey.create!(owner: user, name: "Multi", scopes: %w[read write])
140+
token = key.instance_variable_get(:@token)
141+
request = FakeRequest.new(headers: { "Authorization" => "Bearer #{token}" })
142+
controller = FakeController.new(request)
143+
144+
ApiKeys.configure { |c| c.enable_async_operations = false }
145+
146+
# Succeeds when both required
147+
controller.send(:authenticate_api_key!, scope: %w[read write])
148+
assert_nil controller.rendered, "Should not render when authorized"
149+
150+
# Fails when one required scope missing
151+
key_missing = ApiKeys::ApiKey.create!(owner: user, name: "Missing", scopes: %w[read])
152+
token2 = key_missing.instance_variable_get(:@token)
153+
controller2 = FakeController.new(FakeRequest.new(headers: { "Authorization" => "Bearer #{token2}" }))
154+
controller2.send(:authenticate_api_key!, scope: %w[read write])
155+
refute_nil controller2.rendered
156+
assert_equal :missing_scope, controller2.rendered[:json][:error]
157+
assert_equal %w[read write], controller2.rendered[:json][:required_scope]
158+
end
136159
end
137160
end
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# frozen_string_literal: true
2+
3+
require "test_helper"
4+
5+
# Stub helper_method for non-Rails controller context
6+
module Kernel
7+
def helper_method(*); end
8+
end
9+
10+
module ApiKeys
11+
class TenantResolutionConcernTest < ApiKeys::Test
12+
class FakeRequest
13+
def uuid; SecureRandom.uuid; end
14+
end
15+
16+
class FakeController
17+
include ApiKeys::Authentication
18+
include ApiKeys::TenantResolution
19+
20+
def initialize(api_key)
21+
@api_key = api_key
22+
end
23+
24+
# Minimal request needed for Authentication callbacks
25+
def request
26+
@request ||= FakeRequest.new
27+
end
28+
29+
# Override authenticator usage to set current_api_key directly for isolation
30+
def set_key(api_key)
31+
@current_api_key = api_key
32+
end
33+
34+
# Satisfy render from Authentication even if not used here
35+
def render(json:, status:); end
36+
end
37+
38+
def setup
39+
super
40+
ApiKeys.configure do |c|
41+
c.enable_async_operations = false
42+
c.tenant_resolver = ->(api_key) { api_key.owner if api_key.respond_to?(:owner) }
43+
end
44+
end
45+
46+
test "returns owner by default resolver" do
47+
user = User.create!(name: "Tenant User")
48+
key = ApiKeys::ApiKey.create!(owner: user, name: "TKey")
49+
50+
controller = FakeController.new(key)
51+
controller.set_key(key)
52+
53+
assert_equal user, controller.send(:current_api_tenant)
54+
assert_equal user, controller.send(:current_api_key_tenant)
55+
assert_equal user, controller.send(:current_api_account)
56+
assert_equal user, controller.send(:current_api_owner)
57+
assert_equal user, controller.send(:current_api_key_owner)
58+
end
59+
60+
test "returns custom tenant via resolver" do
61+
org = Struct.new(:id).new(42)
62+
user = User.create!(name: "Org User")
63+
key = ApiKeys::ApiKey.create!(owner: user, name: "Key")
64+
65+
ApiKeys.configure { |c| c.tenant_resolver = ->(api_key) { org } }
66+
controller = FakeController.new(key)
67+
controller.set_key(key)
68+
69+
assert_equal org, controller.send(:current_api_tenant)
70+
end
71+
72+
test "handles resolver errors and returns nil" do
73+
user = User.create!(name: "Err User")
74+
key = ApiKeys::ApiKey.create!(owner: user, name: "Key")
75+
76+
ApiKeys.configure { |c| c.tenant_resolver = ->(_) { raise "boom" } }
77+
controller = FakeController.new(key)
78+
controller.set_key(key)
79+
80+
assert_nil controller.send(:current_api_tenant)
81+
end
82+
83+
test "returns nil when no current_api_key" do
84+
controller = FakeController.new(nil)
85+
controller.set_key(nil)
86+
assert_nil controller.send(:current_api_tenant)
87+
end
88+
end
89+
end

test/models/api_key_test.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,10 @@ def setup
4545
assert_not api_key.allows_scope?("admin")
4646
end
4747

48-
test "creates with bcrypt digest by default" do
49-
api_key = ApiKeys::ApiKey.create!(owner: @user, name: "Bcrypt Key")
50-
assert_equal "bcrypt", api_key.digest_algorithm
51-
assert ApiKeys::Services::Digestor.match?(token: api_key.instance_variable_get(:@token), stored_digest: api_key.token_digest, strategy: :bcrypt)
48+
test "creates with sha256 digest by default" do
49+
api_key = ApiKeys::ApiKey.create!(owner: @user, name: "SHA256 Default")
50+
assert_equal "sha256", api_key.digest_algorithm
51+
assert ApiKeys::Services::Digestor.match?(token: api_key.instance_variable_get(:@token), stored_digest: api_key.token_digest, strategy: :sha256)
5252
end
5353

5454
test "creates with sha256 digest if configured" do

test/services/digestor_test.rb

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,10 @@ def setup
1212

1313
# === .digest ===
1414

15-
test ".digest uses bcrypt by default" do
15+
test ".digest uses sha256 by default" do
1616
result = ApiKeys::Services::Digestor.digest(token: @token)
17-
assert_equal "bcrypt", result[:algorithm]
18-
assert BCrypt::Password.valid_hash?(result[:digest])
19-
assert BCrypt::Password.new(result[:digest]) == @token
17+
assert_equal "sha256", result[:algorithm]
18+
assert_equal Digest::SHA256.hexdigest(@token), result[:digest]
2019
end
2120

2221
test ".digest uses bcrypt when specified" do

test/services/token_generator_test.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ module Services
77
class TokenGeneratorTest < ApiKeys::Test
88
test "generates token with default settings (prefix, length, base58)" do
99
token = ApiKeys::Services::TokenGenerator.call
10-
assert_match(/^ak_test_/, token) # Default prefix for test env
10+
assert_match(/^ak_/, token) # Default prefix
1111
# Base58 length varies slightly, check it's roughly correct
1212
# 24 bytes entropy -> ~32-33 Base58 chars
13-
random_part_length = token.delete_prefix("ak_test_").length
13+
random_part_length = token.delete_prefix("ak_").length
1414
assert_includes 28..60, random_part_length, "Base58 token length out of expected range"
1515
assert token.match?(/^[a-zA-Z0-9_]+$/), "Token contains unexpected characters"
1616
end
@@ -25,15 +25,15 @@ class TokenGeneratorTest < ApiKeys::Test
2525
ApiKeys.configure { |config| config.token_length = 32 } # More entropy
2626
token = ApiKeys::Services::TokenGenerator.call
2727
# 32 bytes entropy -> ~43-44 Base58 chars; allow a generous range to account for encoding variance
28-
random_part_length = token.delete_prefix("ak_test_").length
28+
random_part_length = token.delete_prefix("ak_").length
2929
assert_includes 40..64, random_part_length, "Base58 token length out of expected range for 32 bytes"
3030
end
3131

3232
test "generates token with hex alphabet when configured" do
3333
ApiKeys.configure { |config| config.token_alphabet = :hex }
3434
token = ApiKeys::Services::TokenGenerator.call
35-
assert_match(/^ak_test_/, token)
36-
random_part = token.delete_prefix("ak_test_")
35+
assert_match(/^ak_/, token)
36+
random_part = token.delete_prefix("ak_")
3737
assert_equal ApiKeys.configuration.token_length * 2, random_part.length # Hex is 2 chars per byte
3838
assert random_part.match?(/^[0-9a-f]+$/), "Token contains non-hex characters"
3939
end

0 commit comments

Comments
 (0)