api_keys 0.4.0 → 0.4.1

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: 3183ee0be98a005fd2fc318937f9f8aeb6d8a6c0528a0b75d7234e55cde80480
4
- data.tar.gz: 67080c1cddf08ee647657c8845a9332c61279249d8c9765ea0ad6fda4e4961a3
3
+ metadata.gz: 51db6ca1f89acc5b0638e33db41b88694b8be6275c3528a72012b3e2609d8736
4
+ data.tar.gz: 40c384e1af9d2befca96c4c5a3395daff9059ee19334ee01f997c59396367bdd
5
5
  SHA512:
6
- metadata.gz: ff1adbe9b462443d7f278013030780bfc99ec2ef5acb6a814966a3530950147069bf33cc79d3d35ae8f6af8f76bf43b3a9a42d9692e22f9757df3c172b186535
7
- data.tar.gz: 6f3f9febcd26069d345739d6268c8bcd4acce174ccdd2c24f915c8cfabf5404c2aa708e3538219ad8be30c8392d633101a6f1e52d7b099e123bf01474511859e
6
+ metadata.gz: 8ba7f9f769475fbf703446fa91ba19500393391eb1f1fa9b45eb5772fb46e044f8af492058d306c64bcf657c0c3654846d1d188916e4edaf7ee9e9a68571deed
7
+ data.tar.gz: f6ede6c7e458b1cdbac9cd580e46536ba735891e283c4a4e2eececbc4c194b91e36ca5756a43cf8b3ef80c9d5066fa50d35cdc59efa246543bbc5e85b3ebd352
data/CHANGELOG.md CHANGED
@@ -1,4 +1,21 @@
1
- ## [Unreleased]
1
+ ## [0.4.1] - 2026-08-24
2
+
3
+ ### Fixed
4
+
5
+ - Stop the dashboard's Content Security Policy from blocking the host application's own stylesheets, scripts, and self-hosted webfonts. The 0.4.0 policy declared `default-src 'none'` with `script-src`/`style-src` reduced to the per-request nonce, but Rails only stamps that nonce onto `stylesheet_link_tag`/`javascript_include_tag` when the host application sets `content_security_policy_nonce_auto` (off by default, and not something an engine can enable for its host). Any application rendering its normal layout on engine pages got an unstyled, inert dashboard with no server-side error. The default policy now trusts same-origin scripts, styles, and fonts, adds `font-src`, and allows `https:` images, while keeping the nonce, `default-src 'self'`, `base-uri 'none'`, `object-src 'none'`, `frame-ancestors 'none'`, `frame-src 'none'`, `form-action 'self'`, and `connect-src 'self'`.
6
+ - Stop generated install and authentication-index migrations from carrying an unreachable `migration_version` instance method; the generator already renders the Active Record version into each migration superclass.
7
+
8
+ ### Added
9
+
10
+ - `config.dashboard_content_security_policy` selects the policy the mounted dashboard declares: `:default` (new default, hardened but compatible with a normal host layout), `:strict` (the 0.4.0 nonce-only policy, unchanged), or `false`/`nil` to declare nothing and leave the host application's policy alone. The setting is resolved per request and validated on assignment.
11
+
12
+ ### Upgrade notes
13
+
14
+ - Applications that relied on the 0.4.0 nonce-only dashboard policy should set `config.dashboard_content_security_policy = :strict` to keep it. That policy requires a layout that serves nothing un-nonced on engine pages; the gem's built-in layout satisfies it, and host layouts additionally need `config.content_security_policy_nonce_auto = true`.
15
+
16
+ ### Maintenance
17
+
18
+ - Move SimpleCov startup/reporting into the test helper and replace deprecated filtering/tracking APIs so the enforced line and branch coverage gates remain compatible with future SimpleCov releases.
2
19
 
3
20
  ## [0.4.0] - 2026-08-09
4
21
 
data/README.md CHANGED
@@ -184,6 +184,8 @@ Tweak colors and spacing by overriding CSS variables in your application's style
184
184
  }
185
185
  ```
186
186
 
187
+ Engine pages ship an enforcing Content Security Policy. The default allows same-origin scripts, styles, and fonts, so your own stylesheet loads normally — see [Dashboard Content Security Policy](#dashboard-content-security-policy) to tighten or disable it.
188
+
187
189
  #### Building Custom Integrations
188
190
 
189
191
  If you need complete control over the UI (e.g., to match your design system with Tailwind, Bootstrap, etc.), you can build your own views and controllers while using the gem's model layer and helpers.
@@ -973,6 +975,51 @@ Increase the TTL to reduce repeated lookup work without making cached state auth
973
975
  config.cache_ttl = 2.minutes
974
976
  ```
975
977
 
978
+ ### Dashboard Content Security Policy
979
+
980
+ The mounted dashboard renders credential material, so it declares its own enforcing `Content-Security-Policy` on engine pages even when your application has none. You choose how strict it is:
981
+
982
+ ```ruby
983
+ config.dashboard_content_security_policy = :default # :default (default), :strict, or false/nil
984
+ ```
985
+
986
+ | Value | Behavior |
987
+ |-------|----------|
988
+ | `:default` | Hardened, but compatible with a normal host layout. |
989
+ | `:strict` | Nonce-only: nothing loads unless it carries the engine's per-request nonce. |
990
+ | `false` / `nil` | The gem declares nothing; your application's policy applies unchanged. |
991
+
992
+ **`:default`** emits (nonce regenerated per request):
993
+
994
+ ```
995
+ base-uri 'none'; object-src 'none'; frame-ancestors 'none'; frame-src 'none';
996
+ form-action 'self'; connect-src 'self'; default-src 'self';
997
+ script-src 'self' 'nonce-…'; style-src 'self' 'nonce-…';
998
+ font-src 'self' data:; img-src 'self' https: data:
999
+ ```
1000
+
1001
+ Framing, plugins, `<base>` hijacking, and cross-origin form posts stay blocked, and the dashboard's own inline `<style>`/`<script>` blocks stay nonce-gated — but your layout's stylesheets, scripts, and self-hosted webfonts still load.
1002
+
1003
+ **`:strict`** restores the nonce-only policy: `default-src 'none'` with no source expression for scripts or styles, so the per-request nonce is the only thing that can run.
1004
+
1005
+ ```ruby
1006
+ config.dashboard_content_security_policy = :strict
1007
+ ```
1008
+
1009
+ Only use `:strict` if the layout rendered on engine pages serves nothing un-nonced. Rails does **not** put the nonce on `stylesheet_link_tag` / `javascript_include_tag` unless the host application opts in:
1010
+
1011
+ ```ruby
1012
+ # config/application.rb — required for :strict with a normal host layout
1013
+ config.content_security_policy_nonce_generator = ->(request) { SecureRandom.base64(16) }
1014
+ config.content_security_policy_nonce_auto = true
1015
+ ```
1016
+
1017
+ Without that, `:strict` blocks your layout's own asset tags and the dashboard renders unstyled and inert. The gem's built-in layout is fully self-contained and works under `:strict` as-is.
1018
+
1019
+ **`false` / `nil`** leaves your application's policy completely alone. If your policy is nonce-based, allow the dashboard's nonced inline `<style>` and `<script>` yourself.
1020
+
1021
+ Any directive the gem does not set is inherited from your application's policy.
1022
+
976
1023
 
977
1024
  ## Callbacks: analytics, logging, usage monitoring & auditing
978
1025
 
@@ -18,25 +18,11 @@ module ApiKeys
18
18
  # The dashboard renders credential material, so give it an enforcing,
19
19
  # self-contained CSP even when the host application has not configured one.
20
20
  # Preserve any host directives we do not explicitly tighten.
21
- before_action :prepare_api_keys_content_security_policy_nonce
22
- if respond_to?(:content_security_policy)
23
- content_security_policy do |policy|
24
- policy.default_src :none
25
- policy.base_uri :none
26
- policy.object_src :none
27
- policy.frame_ancestors :none
28
- policy.frame_src :none
29
- policy.form_action :self
30
- # Rails appends the per-request nonce to these directives. `none` leaves
31
- # the nonce as the only effective source instead of trusting every
32
- # same-origin script or stylesheet.
33
- policy.script_src :none
34
- policy.style_src :none
35
- policy.img_src :self, :data
36
- policy.connect_src :self
37
- end
38
- content_security_policy_report_only false
39
- end
21
+ #
22
+ # The policy is selected by `ApiKeys.configuration.dashboard_content_security_policy`
23
+ # and resolved per request, so host applications can relax or disable it from
24
+ # an initializer. See `apply_api_keys_content_security_policy` below.
25
+ before_action :apply_api_keys_content_security_policy
40
26
 
41
27
  # Ensure the owner is authenticated for all actions within this engine
42
28
  # This uses the configured authentication method (defaults to authenticate_user!)
@@ -45,6 +31,54 @@ module ApiKeys
45
31
 
46
32
  private
47
33
 
34
+ # Declares the dashboard's Content Security Policy for this request.
35
+ #
36
+ # `:default` keeps every structural protection (no framing, no plugins, no
37
+ # `<base>` hijacking, same-origin form submission) while still trusting
38
+ # same-origin scripts, styles, and fonts. That matters because Rails only
39
+ # stamps the per-request nonce onto `stylesheet_link_tag` / `javascript_include_tag`
40
+ # when the host sets `config.content_security_policy_nonce_auto`, which is off
41
+ # by default and which this engine cannot turn on for the host. Without a
42
+ # source expression the host layout's own asset tags are blocked and the
43
+ # dashboard renders unstyled and inert.
44
+ #
45
+ # `:strict` restores the nonce-only policy for applications whose layout
46
+ # serves nothing un-nonced on engine pages. `false`/`nil` declares nothing.
47
+ def apply_api_keys_content_security_policy
48
+ mode = ApiKeys.configuration.dashboard_content_security_policy
49
+ return unless mode # false/nil: leave the host application's policy alone
50
+ return unless request.respond_to?(:content_security_policy=)
51
+
52
+ prepare_api_keys_content_security_policy_nonce
53
+
54
+ policy = request.content_security_policy&.clone || ActionDispatch::ContentSecurityPolicy.new
55
+ policy.base_uri :none
56
+ policy.object_src :none
57
+ policy.frame_ancestors :none
58
+ policy.frame_src :none
59
+ policy.form_action :self
60
+ policy.connect_src :self
61
+
62
+ if mode == :strict
63
+ # Rails appends the per-request nonce to script-src/style-src. `none`
64
+ # leaves the nonce as the only effective source instead of trusting
65
+ # every same-origin script or stylesheet.
66
+ policy.default_src :none
67
+ policy.script_src :none
68
+ policy.style_src :none
69
+ policy.img_src :self, :data
70
+ else
71
+ policy.default_src :self
72
+ policy.script_src :self
73
+ policy.style_src :self
74
+ policy.font_src :self, :data
75
+ policy.img_src :self, :https, :data
76
+ end
77
+
78
+ request.content_security_policy = policy
79
+ request.content_security_policy_report_only = false if request.respond_to?(:content_security_policy_report_only=)
80
+ end
81
+
48
82
  def prepare_api_keys_content_security_policy_nonce
49
83
  return unless request.respond_to?(:content_security_policy_nonce_generator=)
50
84
 
@@ -56,6 +56,20 @@ module ApiKeys
56
56
  # Engine UI Configuration
57
57
  attr_accessor :return_url, :return_text
58
58
 
59
+ # Dashboard Content Security Policy
60
+ #
61
+ # @!attribute [rw] dashboard_content_security_policy
62
+ # @return [Symbol, false, nil] Which Content-Security-Policy the mounted
63
+ # dashboard declares for its own pages.
64
+ # - `:default` (default) — hardened but host-compatible: same-origin
65
+ # scripts, styles, and fonts are allowed alongside the engine's
66
+ # per-request nonce, so a host layout's asset tags keep working.
67
+ # - `:strict` — nonce-only: `default-src 'none'` with no source
68
+ # expression for scripts or styles. Requires a host layout that
69
+ # serves no un-nonced assets on engine pages.
70
+ # - `false` / `nil` — declare nothing and leave the host policy alone.
71
+ attr_reader :dashboard_content_security_policy
72
+
59
73
  # Debugging
60
74
  attr_reader :debug_logging
61
75
 
@@ -108,6 +122,7 @@ module ApiKeys
108
122
 
109
123
  VALID_HASH_STRATEGIES = %i[sha256 bcrypt].freeze
110
124
  VALID_TOKEN_ALPHABETS = %i[base58 hex].freeze
125
+ VALID_DASHBOARD_CONTENT_SECURITY_POLICIES = [:default, :strict, false, nil].freeze
111
126
  TOKEN_LENGTH_RANGE = (16..64)
112
127
  MAX_CONFIGURED_SCOPES = 100
113
128
  CONFIG_NAME_PATTERN = /\A[a-zA-Z0-9_-]{1,64}\z/
@@ -294,6 +309,23 @@ module ApiKeys
294
309
  @token_alphabet = value
295
310
  end
296
311
 
312
+ # Selects the Content-Security-Policy the dashboard declares for its own pages.
313
+ # `true` is accepted as an alias for `:default`; Strings are normalized to Symbols.
314
+ def dashboard_content_security_policy=(value)
315
+ normalized = case value
316
+ when true then :default
317
+ when String then value.to_sym
318
+ else value
319
+ end
320
+
321
+ unless VALID_DASHBOARD_CONTENT_SECURITY_POLICIES.include?(normalized)
322
+ raise ArgumentError,
323
+ "dashboard_content_security_policy must be :default, :strict, false, or nil"
324
+ end
325
+
326
+ @dashboard_content_security_policy = normalized
327
+ end
328
+
297
329
  def hash_strategy=(value)
298
330
  unless VALID_HASH_STRATEGIES.include?(value)
299
331
  raise ArgumentError, "hash_strategy must be one of: #{VALID_HASH_STRATEGIES.join(', ')}"
@@ -541,6 +573,12 @@ module ApiKeys
541
573
  @return_url = "/" # Default fallback path
542
574
  @return_text = "‹ Home" # Default link text
543
575
 
576
+ # Dashboard Content Security Policy
577
+ # Hardened, but compatible with a normal host layout: same-origin scripts,
578
+ # styles, and fonts load, and the engine's inline blocks stay nonce-gated.
579
+ # Use :strict for the nonce-only policy, or false/nil to declare nothing.
580
+ @dashboard_content_security_policy = :default
581
+
544
582
  # Debugging
545
583
  @debug_logging = false # Disable debug logging by default (warn and error get logged regardless of this)
546
584
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ApiKeys
4
- VERSION = "0.4.0"
4
+ VERSION = "0.4.1"
5
5
  end
@@ -21,12 +21,4 @@ class AddAuthenticationIndexToApiKeys < ActiveRecord::Migration<%= migration_ver
21
21
  options[:algorithm] = :concurrently if connection.adapter_name.downcase.include?("postgres")
22
22
  remove_index :api_keys, **options
23
23
  end
24
-
25
- private
26
-
27
- def migration_version
28
- major = ActiveRecord::VERSION::MAJOR
29
- minor = ActiveRecord::VERSION::MINOR
30
- major >= 5 ? "[#{major}.#{minor}]" : ""
31
- end
32
24
  end
@@ -94,15 +94,4 @@ class CreateApiKeysTable < ActiveRecord::Migration<%= migration_version %>
94
94
  # Fallback during initial setup or if connection isn't available
95
95
  :text
96
96
  end
97
-
98
- # Provides the appropriate migration version syntax for the current Rails version.
99
- def migration_version
100
- major = ActiveRecord::VERSION::MAJOR
101
- minor = ActiveRecord::VERSION::MINOR
102
- if major >= 5
103
- "[#{major}.#{minor}]"
104
- else
105
- ""
106
- end
107
- end
108
97
  end
@@ -58,6 +58,24 @@ ApiKeys.configure do |config|
58
58
  # Default: "‹ Home"
59
59
  # config.return_text = "‹ Back to Settings"
60
60
 
61
+ # Content Security Policy for the mounted dashboard pages.
62
+ #
63
+ # :default (default) - Hardened, but compatible with a normal host layout.
64
+ # Blocks framing, plugins, <base> hijacking, and
65
+ # cross-origin form posts, while allowing same-origin
66
+ # scripts, styles, and fonts so your layout's asset
67
+ # tags keep working.
68
+ # :strict - Nonce-only: no script or stylesheet loads unless it
69
+ # carries the engine's per-request nonce. Only use this
70
+ # if your layout serves nothing un-nonced on these pages
71
+ # (Rails only nonces asset tags when you also set
72
+ # config.content_security_policy_nonce_auto = true).
73
+ # false / nil - Declare nothing; your application's own policy applies
74
+ # unchanged. You then have to allow the dashboard's
75
+ # nonced inline <style>/<script> yourself.
76
+ #
77
+ # config.dashboard_content_security_policy = :default
78
+
61
79
  # ============================================================================
62
80
  # TOKEN PREFIXES
63
81
  # ============================================================================
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: api_keys
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - rameerez