Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/views/api_keys/keys/_key_actions.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
<%# Locals: key (required) - The ApiKey record %>

<% if key.active? %>
<%= link_to api_keys.edit_key_path(key), title: "Edit Key", class: "api-keys-action-edit" do %>
<%= link_to edit_key_path(key), title: "Edit Key", class: "api-keys-action-edit" do %>
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M16.793 2.793a3.121 3.121 0 1 1 4.414 4.414l-8.5 8.5A1 1 0 0 1 12 16H9a1 1 0 0 1-1-1v-3a1 1 0 0 1 .293-.707l8.5-8.5Zm3 1.414a1.121 1.121 0 0 0-1.586 0L10 12.414V14h1.586l8.207-8.207a1.121 1.121 0 0 0 0-1.586ZM6 5a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-4a1 1 0 1 1 2 0v4a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h4a1 1 0 1 1 0 2H6Z" clip-rule="evenodd"></path></svg>
<% end %>

<% if key.revocable? %>
<%= button_to api_keys.revoke_key_path(key), title: "Revoke Key", class: "api-keys-action-revoke", data: { turbo_method: :post, turbo_confirm: "Are you sure you want to revoke this key? It will stop working immediately." } do %>
<%= button_to revoke_key_path(key), title: "Revoke Key", class: "api-keys-action-revoke", data: { turbo_method: :post, turbo_confirm: "Are you sure you want to revoke this key? It will stop working immediately." } do %>
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M10.556 4a1 1 0 0 0-.97.751l-.292 1.14h5.421l-.293-1.14A1 1 0 0 0 13.453 4h-2.897Zm6.224 1.892-.421-1.639A3 3 0 0 0 13.453 2h-2.897A3 3 0 0 0 7.65 4.253l-.421 1.639H4a1 1 0 1 0 0 2h.1l1.215 11.425A3 3 0 0 0 8.3 22h7.4a3 3 0 0 0 2.984-2.683l1.214-11.425H20a1 1 0 1 0 0-2h-3.22Zm1.108 2H6.112l1.192 11.214A1 1 0 0 0 8.3 20h7.4a1 1 0 0 0 .995-.894l1.192-11.214ZM10 10a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0v-5a1 1 0 0 1 1-1Zm4 0a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0v-5a1 1 0 0 1 1-1Z" clip-rule="evenodd"></path></svg>
<% end %>
<% else %>
Expand Down
2 changes: 1 addition & 1 deletion app/views/api_keys/keys/_show_token.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<% end %>

<p>
<%= link_to api_keys.security_best_practices_path, class: "text-primary api-keys-align-center" do %>
<%= link_to security_best_practices_path, class: "text-primary api-keys-align-center" do %>
Learn more about API key best practices&nbsp;
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M15 5a1 1 0 1 1 0-2h5a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0V6.414l-5.293 5.293a1 1 0 0 1-1.414-1.414L17.586 5H15ZM4 7a3 3 0 0 1 3-3h3a1 1 0 1 1 0 2H7a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1v-3a1 1 0 1 1 2 0v3a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3V7Z" clip-rule="evenodd"></path></svg>
<% end %>
Expand Down
4 changes: 2 additions & 2 deletions app/views/api_keys/keys/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
<% else %>
Do not share your API key with others or expose it in the browser or other client-side code.
<% end %>
<%= link_to api_keys.security_best_practices_path, class: "text-primary api-keys-align-center" do %>
<%= link_to security_best_practices_path, class: "text-primary api-keys-align-center" do %>
Learn more&nbsp;
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M15 5a1 1 0 1 1 0-2h5a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0V6.414l-5.293 5.293a1 1 0 0 1-1.414-1.414L17.586 5H15ZM4 7a3 3 0 0 1 3-3h3a1 1 0 1 1 0 2H7a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1v-3a1 1 0 1 1 2 0v3a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3V7Z" clip-rule="evenodd"></path></svg>
<% end %>
Expand All @@ -46,7 +46,7 @@
<% end %>

<% else %>
<p>Do not share your API key with others or expose it in the browser or other client-side code. <%= link_to api_keys.security_best_practices_path, class: "text-primary api-keys-align-center" do %>
<p>Do not share your API key with others or expose it in the browser or other client-side code. <%= link_to security_best_practices_path, class: "text-primary api-keys-align-center" do %>
Learn more&nbsp;
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M15 5a1 1 0 1 1 0-2h5a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0V6.414l-5.293 5.293a1 1 0 0 1-1.414-1.414L17.586 5H15ZM4 7a3 3 0 0 1 3-3h3a1 1 0 1 1 0 2H7a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1v-3a1 1 0 1 1 2 0v3a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3V7Z" clip-rule="evenodd"></path></svg>
<% end %>
Expand Down
2 changes: 1 addition & 1 deletion app/views/api_keys/security/best_practices.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,6 @@ Rails.application.credentials.your_service_api_key</code></pre>

<hr>

<p><%= link_to "Back to API Keys", api_keys.keys_path, class: "text-primary" %></p>
<p><%= link_to "Back to API Keys", keys_path, class: "text-primary" %></p>

</article>
7 changes: 6 additions & 1 deletion lib/generators/api_keys/templates/initializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,12 @@
# When key_types IS configured and you specify a key_type, this setting
# is IGNORED - the prefix comes from the key type's configuration instead.
#
# WARNING: Once set, do NOT change or existing keys will fail authentication!
# Changing this later is SAFE for existing keys: every key stores its own
# prefix, so authentication keeps finding keys minted under retired
# prefixes (sha256 looks up by pure token digest; bcrypt falls back to a
# cached scan of all prefixes present in the database). Only NEW keys wear
# the new prefix. The one cost: under :bcrypt, keys off the configured
# prefix take the slightly slower known-prefixes lookup path.
# Default: -> { "ak_" }
# config.token_prefix = -> { "myapp_" }

Expand Down
5 changes: 5 additions & 0 deletions test/dummy/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
# Mount the ApiKeys engine for a hosted portal for managing keys
mount ApiKeys::Engine => '/settings/api-keys'

# Second mount under a CUSTOM route name: engine views must keep working
# without the default `api_keys.` routes proxy (regression coverage for
# hardcoded proxy calls — see test/integration/custom_mount_name_test.rb).
mount ApiKeys::Engine => '/renamed-keys', as: :renamed_keys

# Define routes for the demo controller
root "api_keys#index"

Expand Down
31 changes: 31 additions & 0 deletions test/services/authenticator_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,37 @@ def mock_cache(read_map = {})
ApiKeys::Services::Authenticator.call(request)
mock_callback.verify
end

# === Prefix changes are SAFE for existing keys ===
# The initializer template used to warn "Once set, do NOT change or
# existing keys will fail authentication!" — false on both strategies:
# sha256 looks up by pure token digest (prefix never consulted), and
# bcrypt scopes by the key's OWN stored prefix via the known-prefixes
# scan. These pin that guarantee so it can't regress silently.

test "existing sha256 keys keep authenticating after token_prefix changes" do
# @token was minted under the default "ak_" prefix in setup.
ApiKeys.configuration.token_prefix = -> { "vdb_" }

result = Authenticator.call(mock_request(headers: { "Authorization" => "Bearer #{@token}" }))

assert result.success?, "a prefix change must never strand existing keys"
assert_equal @api_key.id, result.api_key.id
end

test "existing bcrypt keys keep authenticating after token_prefix changes" do
with_hash_strategy(:bcrypt) do
key = ApiKeys::ApiKey.create!(owner: @user, name: "Pre-rebrand Key")
token = key.token

ApiKeys.configuration.token_prefix = -> { "vdb_" }

result = Authenticator.call(mock_request(headers: { "Authorization" => "Bearer #{token}" }))

assert result.success?, "the known-prefixes scan must find keys minted under retired prefixes"
assert_equal key.id, result.api_key.id
end
end
end
end
end
33 changes: 33 additions & 0 deletions test/views_route_helpers_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# frozen_string_literal: true

require "test_helper"

module ApiKeys
# Engine views must use their own (engine-relative) route helpers, never
# the host-side routes proxy. `api_keys.keys_path` works only when the
# host mounts the engine under its default name — a custom mount
# (`mount ApiKeys::Engine => "...", as: :settings_api_keys`) renames the
# proxy and every hardcoded call explodes with
# `undefined local variable 'api_keys'`. Inside engine views the bare
# helpers (`keys_path`) resolve against the engine's OWN routes under any
# mount name, so the proxy is never needed there.
#
# This is a source lint rather than a rendered test because the suite
# deliberately runs without booting the dummy app; the dummy's second
# mount (`/renamed-keys`, custom `as:`) covers manual verification.
class ViewsRouteHelpersTest < ApiKeys::Test
VIEWS_GLOB = File.expand_path("../app/views/api_keys/**/*.erb", __dir__)
PROXY_CALL = /\bapi_keys\.\w+_(?:path|url)\b/

test "no engine view calls the host-side api_keys routes proxy" do
offenders = Dir.glob(VIEWS_GLOB).filter_map do |file|
matches = File.read(file).scan(PROXY_CALL)
[ file, matches ] if matches.any?
end

assert_empty offenders,
"Engine views must use engine-relative helpers (keys_path, not api_keys.keys_path); " \
"offenders: #{offenders.map(&:first).join(", ")}"
end
end
end
Loading