Skip to content

Commit 64ff5f2

Browse files
rameerezclaude
andcommitted
Deny blank scopes in key_types mode instead of granting full access
Previously, allows_scope? treated blank scopes as "unrestricted", allowing all operations. This is correct for simple mode (no key_types) where scopes are opt-in, but dangerous when key_types with permission ceilings are configured — a key with empty scopes would silently bypass the entire scope enforcement system. Now checks whether key_types mode is active: blank scopes means "no access" in key_types mode, "unrestricted" in simple mode. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 56e4ead commit 64ff5f2

File tree

2 files changed

+38
-5
lines changed

2 files changed

+38
-5
lines changed

lib/api_keys/models/api_key.rb

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -184,13 +184,23 @@ def destroy!
184184
end
185185

186186
# Basic scope check. Assumes scopes are stored as an array of strings.
187-
# Returns true if the key has no specific scopes (allowing all) or includes the required scope.
187+
#
188+
# Behavior depends on whether key_types mode is enabled:
189+
# - Simple mode (no key_types): blank scopes means "unrestricted" (all scopes allowed).
190+
# This preserves backwards compatibility for apps that don't use scopes at all.
191+
# - Key types mode: blank scopes means "no permissions". When you've configured
192+
# key types with permission ceilings, an empty scope list should deny access,
193+
# not silently bypass the entire permission system.
188194
def allows_scope?(required_scope)
189-
# Type casting for scopes/metadata happens via the attribute definition in the engine.
190-
# Ensure the attribute is loaded/defined before using it.
191-
# Check if the attribute method exists before calling .blank? or .include?
192195
return true unless respond_to?(:scopes) # Guard clause if loaded before attribute definition
193-
scopes.blank? || scopes.include?(required_scope.to_s)
196+
197+
if scopes.blank?
198+
# In key_types mode, blank scopes = no access (deny by default)
199+
# In simple mode, blank scopes = unrestricted (allow by default)
200+
return !ApiKeys.configuration.key_types.present?
201+
end
202+
203+
scopes.include?(required_scope.to_s)
194204
end
195205

196206
# Alias for scopes - provides a more user-friendly API that matches

test/models/api_key_test.rb

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

48+
test "allows_scope? with blank scopes in simple mode allows all" do
49+
# In simple mode (no key_types configured), blank scopes = unrestricted
50+
api_key = ApiKeys::ApiKey.create!(owner: @user, name: "No Scopes", scopes: [])
51+
assert api_key.allows_scope?("read")
52+
assert api_key.allows_scope?("admin")
53+
assert api_key.allows_scope?("anything")
54+
end
55+
56+
test "allows_scope? with blank scopes in key_types mode denies all" do
57+
# In key_types mode, blank scopes = no permissions
58+
original_key_types = ApiKeys.configuration.key_types
59+
ApiKeys.configuration.key_types = {
60+
publishable: { prefix: "pk", permissions: %w[read] },
61+
secret: { prefix: "sk", permissions: :all }
62+
}
63+
64+
api_key = ApiKeys::ApiKey.create!(owner: @user, name: "Empty Scopes", scopes: [])
65+
assert_not api_key.allows_scope?("read")
66+
assert_not api_key.allows_scope?("admin")
67+
ensure
68+
ApiKeys.configuration.key_types = original_key_types
69+
end
70+
4871
test "creates with sha256 digest by default" do
4972
api_key = ApiKeys::ApiKey.create!(owner: @user, name: "SHA256 Default")
5073
assert_equal "sha256", api_key.digest_algorithm

0 commit comments

Comments
 (0)