activeadmin-device_authorization 0.1.3 → 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: 17955a07d8a7c82ff64a20e93c37a448417e69dad7581fb3c80bd087db93210f
4
- data.tar.gz: 5d6a98485620a0501dc4325c5c40b7e7eb2c895d8793dfe6504e91ee937b07db
3
+ metadata.gz: 6edcb0a43663f2ee55e360f2571a9d980265076434205a603dbe4b41a785d351
4
+ data.tar.gz: 438551d1316c58ace7c9b6a81aec3f388a8a261dffdb29d40590348eb9da33e0
5
5
  SHA512:
6
- metadata.gz: 8c6a2334c39150906f51ff4cd2ac98c595752c0b318051a2d3823fb83f3b8b27d32d1a641294451440acba61a4b0892bbce07de62447bc1d959d9db55ac9aa42
7
- data.tar.gz: ea90ab0aa9708c0eb4f9232782ac28f688cd1e9608ecef79e5207ed77550e53ebe98ceca5c9e51bb9c37ab321ebb25c2e548ff026825c8f7cdf4837165fab58a
6
+ metadata.gz: ae06adfc6827bd0c6be364d76716bc0771c4f0012b21a4a6c23cdee93ed395c29b02ad8c60cac0ba0c3bef4bf950767b0bc6ad9b10611da849500810a70dca39
7
+ data.tar.gz: da1d891cadd3eb99c7fa0391d6dfddb306baac964cc66599d932fb12ca6acd742d9ab2766d2a51466ef88e9f5d839ec3aee046f7ae995282916008c7537fddbf
data/CHANGELOG.md CHANGED
@@ -1,6 +1,11 @@
1
1
  # CHANGELOG
2
2
 
3
- ## Unreleased
3
+ ## 0.2.0 (2026-10-28)
4
+
5
+ - BREAKING: Require ActiveAdmin 4 (`>= 4.0.0.beta19`); drop ActiveAdmin 3 support
6
+ - Ship ActiveAdmin 4 consent markup: `.attributes-table`, self-contained verify prompt, Approve/Deny submits inside `.formtastic`
7
+ - Resolve `current_resource_owner` with `instance_exec` for zero-arity procs so private Devise helpers work
8
+ - Document ActionPolicy `approve?` / `deny?` / `step_up?` page policy hooks
4
9
 
5
10
  ## 0.1.3 (2026-10-26)
6
11
 
data/README.md CHANGED
@@ -8,7 +8,7 @@ RFC 8628-style device authorization for ActiveAdmin: public device/token endpoin
8
8
 
9
9
  - Ruby 3.4+
10
10
  - Rails 7.2.3.2+
11
- - ActiveAdmin 3.2+
11
+ - ActiveAdmin 4.0.0.beta19+ (Tailwind theme; ActiveAdmin 3 is unsupported)
12
12
 
13
13
  ## Install
14
14
 
@@ -52,7 +52,11 @@ ActiveAdmin::DeviceAuthorization.configure do |config|
52
52
  config.client_class_name = "DeviceAuthorizationClient"
53
53
  config.grant_class_name = "DeviceAuthorizationGrant"
54
54
  config.access_token_class_name = "DeviceAuthorizationAccessToken"
55
- config.current_resource_owner = ->(controller) { controller.current_admin_user }
55
+ # Prefer zero-arity: the gem runs this with instance_exec on the controller.
56
+ # Devise helpers are often private; do not call them with an explicit receiver.
57
+ config.current_resource_owner = -> { current_admin_user }
58
+ # Arity-1 form still works; use send when the helper is private:
59
+ # config.current_resource_owner = ->(controller) { controller.send(:current_admin_user) }
56
60
 
57
61
  config.scopes = %w[api:read api:write]
58
62
  config.default_scopes = %w[api:read api:write]
@@ -108,6 +112,28 @@ The gem does not replace host security controls. Before production traffic:
108
112
  - Schedule `bundle exec rake active_admin:device_authorization:retention` at the cadence required by the host retention policy. Token retention deletes rows past `token_retention_period` after absolute expiry, revoke, or idle expiry (when idle TTL is enabled). Do not rely on absolute expiry alone when absolute TTL is nil (far-future sentinel).
109
113
  - Implement host models and indexes that match the contract below, including uniqueness and foreign keys.
110
114
  - Authenticate bearer tokens in the host API (see Bearer validation contract). The gem issues and expires tokens; it does not install API authentication middleware.
115
+ - When the host authorizes ActiveAdmin pages with ActionPolicy (or a similar adapter that maps actions to `action_name?` methods), define `approve?`, `deny?`, and `step_up?` on the page policy. Missing methods fail closed and Approve/Deny/step-up return unauthorized. `step_up?` is needed only when `on_approve_step_up` is configured.
116
+
117
+ ```ruby
118
+ # Example ActionPolicy page policy hooks for Device Auth
119
+ class ActiveAdmin::PagePolicy < ApplicationPolicy
120
+ def show?
121
+ true
122
+ end
123
+
124
+ def approve?
125
+ show?
126
+ end
127
+
128
+ def deny?
129
+ show?
130
+ end
131
+
132
+ def step_up?
133
+ show?
134
+ end
135
+ end
136
+ ```
111
137
 
112
138
  ### Host model contract
113
139
 
@@ -145,7 +171,7 @@ make lint
145
171
  make test
146
172
  ```
147
173
 
148
- Matrixed Rails versions use [Appraisal](https://github.com/thoughtbot/appraisal): `gemfiles/rails72.gemfile`, `rails8ruby34.gemfile`, and `rails8ruby4.gemfile`. Run `bundle exec appraisal rspec` to execute RSpec in each gemfile context.
174
+ Matrixed Rails versions use [Appraisal](https://github.com/thoughtbot/appraisal): `gemfiles/rails72.gemfile`, `rails8ruby34.gemfile`, and `rails8ruby4.gemfile`, each pinned to ActiveAdmin 4.0.0.beta23. Run `bundle exec appraisal rspec` to execute RSpec in each gemfile context.
149
175
 
150
176
  [Trunk](https://docs.trunk.io) config lives in `.trunk/`; CI can run `trunk` via `.github/workflows/_trunk_check.yml`. Releases: `make release` or `usr/bin/release.rb`.
151
177
 
@@ -43,7 +43,7 @@ Gem::Specification.new do |spec|
43
43
 
44
44
  spec.require_paths = ["lib"]
45
45
 
46
- spec.add_dependency "activeadmin", ">= 3.2"
46
+ spec.add_dependency "activeadmin", ">= 4.0.0.beta19", "< 5"
47
47
  spec.add_dependency "rails", ">= 7.2.3.2"
48
48
 
49
49
  spec.add_development_dependency "appraisal", "~> 2"
@@ -66,7 +66,7 @@ module ActiveAdmin
66
66
  end
67
67
 
68
68
  def current_resource_owner
69
- @current_resource_owner ||= ActiveAdmin::DeviceAuthorization.config.current_resource_owner.call(self)
69
+ @current_resource_owner ||= ActiveAdmin::DeviceAuthorization.config.resolve_current_resource_owner(self)
70
70
  end
71
71
 
72
72
  def load_grant!
@@ -10,18 +10,25 @@
10
10
  font-size: 1.1em;
11
11
  letter-spacing: 0.08em;
12
12
  }
13
- .device-auth-actions ol {
13
+ .device-auth-verify {
14
+ border: 1px solid #8a2f1d;
15
+ border-radius: 0.25rem;
16
+ background: #f8e8e4;
17
+ color: #5c1f14;
18
+ margin: 0 0 1rem;
19
+ padding: 0.75rem 0.9rem;
20
+ }
21
+ .device-auth-actions {
14
22
  display: flex;
15
23
  flex-direction: column;
16
24
  gap: 0.65rem;
17
- list-style: none;
18
- margin: 0;
19
- padding: 0;
25
+ margin-top: 0.75rem;
20
26
  }
21
27
  @media (min-width: 40rem) {
22
- .device-auth-actions ol {
28
+ .device-auth-actions {
23
29
  flex-direction: row;
24
30
  flex-wrap: wrap;
31
+ align-items: flex-end;
25
32
  }
26
33
  }
27
34
  </style>
@@ -35,24 +42,26 @@
35
42
 
36
43
  <div class="panel">
37
44
  <div class="panel-body">
38
- <table class="attributes_table">
39
- <tbody>
40
- <tr>
41
- <th scope="row"><%= t("active_admin.device_authorization.client") %></th>
42
- <td><%= client.name %></td>
43
- </tr>
44
- <tr>
45
- <th scope="row"><%= t("active_admin.device_authorization.user_code") %></th>
46
- <td><code class="device-auth-user-code"><%= grant.user_code %></code></td>
47
- </tr>
48
- <tr>
49
- <th scope="row"><%= t("active_admin.device_authorization.status") %></th>
50
- <td><%= grant.status %></td>
51
- </tr>
52
- </tbody>
53
- </table>
45
+ <div class="attributes-table">
46
+ <table>
47
+ <tbody>
48
+ <tr>
49
+ <th scope="row"><%= t("active_admin.device_authorization.client") %></th>
50
+ <td><%= client.name %></td>
51
+ </tr>
52
+ <tr>
53
+ <th scope="row"><%= t("active_admin.device_authorization.user_code") %></th>
54
+ <td><code class="device-auth-user-code"><%= grant.user_code %></code></td>
55
+ </tr>
56
+ <tr>
57
+ <th scope="row"><%= t("active_admin.device_authorization.status") %></th>
58
+ <td><%= grant.status %></td>
59
+ </tr>
60
+ </tbody>
61
+ </table>
62
+ </div>
54
63
 
55
- <div class="flash flash_alert" role="status">
64
+ <div class="device-auth-verify" role="status">
56
65
  <%= t("active_admin.device_authorization.verify_code") %>
57
66
  </div>
58
67
 
@@ -61,49 +70,55 @@
61
70
  <% end %>
62
71
 
63
72
  <% if grant.pending? %>
64
- <%= form_with url: send(:"#{config.path_namespace}_approve_device_auth_path", grant.user_code), method: :post, id: "device-auth-approve", data: {turbo: false}, class: "formtastic" do %>
65
- <% if available_scopes.any? %>
66
- <fieldset class="inputs">
67
- <legend><%= t("active_admin.device_authorization.scopes") %></legend>
68
- <p class="inline-hints"><%= t("active_admin.device_authorization.scopes_hint") %></p>
69
- <% available_scopes.each do |scope| %>
70
- <% field_id = "scope_#{scope.tr(":", "_")}" %>
71
- <li class="choice">
72
- <label for="<%= field_id %>">
73
- <%= check_box_tag "scopes[]", scope, default_scopes.include?(scope), id: field_id %>
74
- <%= config.label_for_scope(scope) %>
75
- </label>
73
+ <div class="device-auth-actions">
74
+ <%= form_with url: send(:"#{config.path_namespace}_approve_device_auth_path", grant.user_code), method: :post, id: "device-auth-approve", data: {turbo: false}, class: "formtastic" do %>
75
+ <% if available_scopes.any? %>
76
+ <fieldset class="inputs">
77
+ <legend><%= t("active_admin.device_authorization.scopes") %></legend>
78
+ <p class="inline-hints"><%= t("active_admin.device_authorization.scopes_hint") %></p>
79
+ <% available_scopes.each do |scope| %>
80
+ <% field_id = "scope_#{scope.tr(":", "_")}" %>
81
+ <li class="choice">
82
+ <label for="<%= field_id %>">
83
+ <%= check_box_tag "scopes[]", scope, default_scopes.include?(scope), id: field_id %>
84
+ <%= config.label_for_scope(scope) %>
85
+ </label>
86
+ </li>
87
+ <% end %>
88
+ </fieldset>
89
+ <% end %>
90
+ <% if config.absolute_expiry_enabled? && absolute_ttl_choices.any? %>
91
+ <fieldset class="inputs">
92
+ <div class="input select">
93
+ <label class="label" for="absolute_ttl_seconds"><%= t("active_admin.device_authorization.absolute_ttl") %></label>
94
+ <%= select_tag :absolute_ttl_seconds,
95
+ options_for_select(
96
+ absolute_ttl_choices.map { |seconds| ["#{seconds.to_i / 3600} h", seconds.to_i] },
97
+ config.access_token_default_absolute_ttl.to_i
98
+ ),
99
+ id: "absolute_ttl_seconds" %>
100
+ </div>
101
+ </fieldset>
102
+ <% end %>
103
+ <fieldset class="actions">
104
+ <ol>
105
+ <li class="action input_action">
106
+ <%= submit_tag t("active_admin.device_authorization.approve") %>
76
107
  </li>
77
- <% end %>
108
+ </ol>
78
109
  </fieldset>
79
110
  <% end %>
80
- <% if config.absolute_expiry_enabled? && absolute_ttl_choices.any? %>
81
- <fieldset class="inputs">
82
- <div class="input select">
83
- <label class="label" for="absolute_ttl_seconds"><%= t("active_admin.device_authorization.absolute_ttl") %></label>
84
- <%= select_tag :absolute_ttl_seconds,
85
- options_for_select(
86
- absolute_ttl_choices.map { |seconds| ["#{seconds.to_i / 3600} h", seconds.to_i] },
87
- config.access_token_default_absolute_ttl.to_i
88
- ),
89
- id: "absolute_ttl_seconds" %>
90
- </div>
111
+
112
+ <%= form_with url: send(:"#{config.path_namespace}_deny_device_auth_path", grant.user_code), method: :post, data: {turbo: false}, class: "formtastic" do %>
113
+ <fieldset class="actions">
114
+ <ol>
115
+ <li class="action input_action">
116
+ <%= submit_tag t("active_admin.device_authorization.deny") %>
117
+ </li>
118
+ </ol>
91
119
  </fieldset>
92
120
  <% end %>
93
- <% end %>
94
-
95
- <fieldset class="actions device-auth-actions">
96
- <ol>
97
- <li class="action input_action">
98
- <%= submit_tag t("active_admin.device_authorization.approve"), form: "device-auth-approve" %>
99
- </li>
100
- <li class="action input_action">
101
- <%= form_with url: send(:"#{config.path_namespace}_deny_device_auth_path", grant.user_code), method: :post, data: {turbo: false} do %>
102
- <%= submit_tag t("active_admin.device_authorization.deny") %>
103
- <% end %>
104
- </li>
105
- </ol>
106
- </fieldset>
121
+ </div>
107
122
  <% else %>
108
123
  <p><%= t("active_admin.device_authorization.not_actionable", status: grant.status) %></p>
109
124
  <% end %>
@@ -34,19 +34,22 @@
34
34
  .device-auth-standalone .panel-body > *:last-child {
35
35
  margin-bottom: 0;
36
36
  }
37
- .device-auth-standalone .attributes_table {
38
- border-collapse: collapse;
37
+ .device-auth-standalone .attributes-table {
39
38
  margin: 0 0 1rem;
40
39
  width: 100%;
41
40
  }
42
- .device-auth-standalone .attributes_table th,
43
- .device-auth-standalone .attributes_table td {
41
+ .device-auth-standalone .attributes-table table {
42
+ border-collapse: collapse;
43
+ width: 100%;
44
+ }
45
+ .device-auth-standalone .attributes-table th,
46
+ .device-auth-standalone .attributes-table td {
44
47
  border-bottom: 1px solid #ddd;
45
48
  padding: 0.55rem 0.35rem;
46
49
  text-align: left;
47
50
  vertical-align: top;
48
51
  }
49
- .device-auth-standalone .attributes_table th {
52
+ .device-auth-standalone .attributes-table th {
50
53
  color: #444;
51
54
  font-weight: 600;
52
55
  width: 8.5rem;
@@ -56,15 +59,13 @@
56
59
  font-size: 1.15rem;
57
60
  letter-spacing: 0.08em;
58
61
  }
59
- .device-auth-standalone .flash {
62
+ .device-auth-standalone .device-auth-verify {
60
63
  border: 1px solid #8a2f1d;
61
64
  border-radius: 2px;
62
- margin: 0 0 1rem;
63
- padding: 0.75rem 0.9rem;
64
- }
65
- .device-auth-standalone .flash_alert {
66
65
  background: #f8e8e4;
67
66
  color: #5c1f14;
67
+ margin: 0 0 1rem;
68
+ padding: 0.75rem 0.9rem;
68
69
  }
69
70
  .device-auth-standalone .inline-hints {
70
71
  color: #555;
@@ -109,10 +110,12 @@
109
110
  outline: 3px solid #1d4f91;
110
111
  outline-offset: 2px;
111
112
  }
112
- .device-auth-standalone .device-auth-actions ol {
113
+ .device-auth-standalone .device-auth-actions {
113
114
  display: flex;
114
115
  flex-direction: column;
115
116
  gap: 0.65rem;
117
+ }
118
+ .device-auth-standalone .device-auth-actions ol {
116
119
  list-style: none;
117
120
  margin: 0;
118
121
  padding: 0;
@@ -129,15 +132,16 @@
129
132
  padding: 0.45rem 1rem;
130
133
  width: 100%;
131
134
  }
132
- .device-auth-standalone .device-auth-actions li:last-child input[type="submit"] {
135
+ .device-auth-standalone .device-auth-actions .formtastic:last-child input[type="submit"] {
133
136
  background: #fff;
134
137
  border-color: #888;
135
138
  color: #222;
136
139
  }
137
140
  @media (min-width: 40rem) {
138
- .device-auth-standalone .device-auth-actions ol {
141
+ .device-auth-standalone .device-auth-actions {
139
142
  flex-direction: row;
140
143
  flex-wrap: wrap;
144
+ align-items: flex-end;
141
145
  }
142
146
  .device-auth-standalone .device-auth-actions input[type="submit"],
143
147
  .device-auth-standalone fieldset.actions input[type="submit"] {
@@ -62,7 +62,9 @@ module ActiveAdmin
62
62
  @client_class_name = "DeviceAuthorizationClient"
63
63
  @grant_class_name = "DeviceAuthorizationGrant"
64
64
  @access_token_class_name = "DeviceAuthorizationAccessToken"
65
- @current_resource_owner = ->(controller) { controller.send(:current_admin_user) }
65
+ # Zero-arity: resolved with instance_exec so Devise helpers work without
66
+ # an explicit receiver (current_admin_user / current_user are often private).
67
+ @current_resource_owner = -> { current_admin_user }
66
68
 
67
69
  # RFC 8628 example uses 1800s; Doorkeeper device grant defaults to 300s.
68
70
  # Ten minutes is a practical admin-consent window.
@@ -146,6 +148,22 @@ module ActiveAdmin
146
148
  key
147
149
  end
148
150
 
151
+ # Resolves the signed-in resource owner for consent actions.
152
+ # Zero-arity callables run via instance_exec on the controller so private
153
+ # Devise helpers (current_admin_user, current_user) are reachable.
154
+ # Arity-1 callables still receive the controller; use send(:current_user)
155
+ # when calling with an explicit receiver.
156
+ def resolve_current_resource_owner(controller)
157
+ callable = current_resource_owner
158
+ return nil if callable.nil?
159
+
160
+ if callable.is_a?(Proc) && callable.arity == 0
161
+ controller.instance_exec(&callable)
162
+ else
163
+ callable.call(controller)
164
+ end
165
+ end
166
+
149
167
  def normalize_scopes(requested)
150
168
  allowed = Array(scopes).map(&:to_s)
151
169
  Array(requested).flatten.compact.map(&:to_s).map(&:strip).uniq.select { |scope| allowed.include?(scope) }
@@ -49,7 +49,7 @@ module ActiveAdmin
49
49
 
50
50
  ActiveAdmin::DeviceAuthorization::Consent.new(
51
51
  grant: @grant,
52
- resource_owner: config.current_resource_owner.call(self),
52
+ resource_owner: config.resolve_current_resource_owner(self),
53
53
  scopes: Array(params[:scopes]),
54
54
  absolute_ttl_seconds: params[:absolute_ttl_seconds]
55
55
  ).approve!
@@ -67,7 +67,7 @@ module ActiveAdmin
67
67
  load_grant!
68
68
  ActiveAdmin::DeviceAuthorization::Consent.new(
69
69
  grant: @grant,
70
- resource_owner: config.current_resource_owner.call(self),
70
+ resource_owner: config.resolve_current_resource_owner(self),
71
71
  scopes: @grant.requested_scopes,
72
72
  absolute_ttl_seconds: config.access_token_default_absolute_ttl
73
73
  ).deny!
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ActiveAdmin
4
4
  module DeviceAuthorization
5
- VERSION = "0.1.3"
5
+ VERSION = "0.2.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activeadmin-device_authorization
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Makarov
@@ -15,14 +15,20 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: '3.2'
18
+ version: 4.0.0.beta19
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '5'
19
22
  type: :runtime
20
23
  prerelease: false
21
24
  version_requirements: !ruby/object:Gem::Requirement
22
25
  requirements:
23
26
  - - ">="
24
27
  - !ruby/object:Gem::Version
25
- version: '3.2'
28
+ version: 4.0.0.beta19
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '5'
26
32
  - !ruby/object:Gem::Dependency
27
33
  name: rails
28
34
  requirement: !ruby/object:Gem::Requirement