activeadmin-device_authorization 0.1.3

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.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +25 -0
  3. data/CODE_OF_CONDUCT.md +26 -0
  4. data/CONTRIBUTING.md +23 -0
  5. data/GOVERNANCE.md +25 -0
  6. data/LICENSE.md +21 -0
  7. data/README.md +162 -0
  8. data/SECURITY.md +25 -0
  9. data/activeadmin-device_authorization.gemspec +62 -0
  10. data/app/controllers/active_admin/device_authorization/application_controller.rb +9 -0
  11. data/app/controllers/active_admin/device_authorization/consents_controller.rb +83 -0
  12. data/app/controllers/active_admin/device_authorization/device_authorizations_controller.rb +51 -0
  13. data/app/controllers/active_admin/device_authorization/json_response.rb +25 -0
  14. data/app/controllers/active_admin/device_authorization/tokens_controller.rb +39 -0
  15. data/app/views/active_admin/device_authorization/_consent.html.erb +111 -0
  16. data/app/views/active_admin/device_authorization/_enter_code.html.erb +12 -0
  17. data/app/views/active_admin/device_authorization/_standalone_styles.html.erb +147 -0
  18. data/app/views/active_admin/device_authorization/consents/index.html.erb +19 -0
  19. data/app/views/active_admin/device_authorization/consents/show.html.erb +14 -0
  20. data/config/locales/en.yml +29 -0
  21. data/config/locales/fi.yml +29 -0
  22. data/config/locales/sv.yml +29 -0
  23. data/config/polyrun_coverage.yml +8 -0
  24. data/lib/active_admin/device_authorization/access_token_expiry.rb +46 -0
  25. data/lib/active_admin/device_authorization/callback.rb +15 -0
  26. data/lib/active_admin/device_authorization/configuration.rb +229 -0
  27. data/lib/active_admin/device_authorization/consent.rb +65 -0
  28. data/lib/active_admin/device_authorization/device_authorization_request.rb +70 -0
  29. data/lib/active_admin/device_authorization/engine.rb +17 -0
  30. data/lib/active_admin/device_authorization/page.rb +126 -0
  31. data/lib/active_admin/device_authorization/retention.rb +39 -0
  32. data/lib/active_admin/device_authorization/routes.rb +60 -0
  33. data/lib/active_admin/device_authorization/token_exchange.rb +144 -0
  34. data/lib/active_admin/device_authorization/user_code.rb +48 -0
  35. data/lib/active_admin/device_authorization/version.rb +7 -0
  36. data/lib/active_admin/device_authorization.rb +41 -0
  37. data/lib/activeadmin-device_authorization.rb +3 -0
  38. data/lib/tasks/active_admin_device_authorization.rake +10 -0
  39. data/sig/active_admin/device_authorization.rbs +5 -0
  40. metadata +296 -0
@@ -0,0 +1,111 @@
1
+ <% config = ActiveAdmin::DeviceAuthorization.config %>
2
+ <% available_scopes = grant.requested_scopes %>
3
+ <% default_scopes = Array(config.default_scopes).map(&:to_s) & available_scopes %>
4
+ <% absolute_ttl_choices = Array(config.access_token_absolute_ttl_choices) %>
5
+ <% client = grant.public_send(config.client_class_name.underscore) %>
6
+ <% heading_level = local_assigns.fetch(:heading_level, :h3) %>
7
+ <style>
8
+ .device-auth-user-code {
9
+ font-family: ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas, monospace;
10
+ font-size: 1.1em;
11
+ letter-spacing: 0.08em;
12
+ }
13
+ .device-auth-actions ol {
14
+ display: flex;
15
+ flex-direction: column;
16
+ gap: 0.65rem;
17
+ list-style: none;
18
+ margin: 0;
19
+ padding: 0;
20
+ }
21
+ @media (min-width: 40rem) {
22
+ .device-auth-actions ol {
23
+ flex-direction: row;
24
+ flex-wrap: wrap;
25
+ }
26
+ }
27
+ </style>
28
+
29
+ <div class="panel">
30
+ <%= content_tag heading_level, t("active_admin.device_authorization.title"), class: "panel-title" %>
31
+ <div class="panel-body">
32
+ <p><%= t("active_admin.device_authorization.intro") %></p>
33
+ </div>
34
+ </div>
35
+
36
+ <div class="panel">
37
+ <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>
54
+
55
+ <div class="flash flash_alert" role="status">
56
+ <%= t("active_admin.device_authorization.verify_code") %>
57
+ </div>
58
+
59
+ <% if config.idle_expiry_enabled? %>
60
+ <p class="inline-hints"><%= t("active_admin.device_authorization.idle_note", idle_hours: (config.access_token_idle_ttl.to_i / 3600.0).round(1)) %></p>
61
+ <% end %>
62
+
63
+ <% 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>
76
+ </li>
77
+ <% end %>
78
+ </fieldset>
79
+ <% 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>
91
+ </fieldset>
92
+ <% 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>
107
+ <% else %>
108
+ <p><%= t("active_admin.device_authorization.not_actionable", status: grant.status) %></p>
109
+ <% end %>
110
+ </div>
111
+ </div>
@@ -0,0 +1,12 @@
1
+ <p class="inline-hints"><%= t("active_admin.device_authorization.enter_code_hint") %></p>
2
+ <%= form_with url: send(:"#{ActiveAdmin::DeviceAuthorization.config.path_namespace}_device_auth_path"), method: :get, local: true, class: "formtastic" do %>
3
+ <fieldset class="inputs">
4
+ <div class="input string required">
5
+ <label class="label" for="user_code"><%= t("active_admin.device_authorization.user_code") %></label>
6
+ <%= text_field_tag :user_code, params[:user_code], id: "user_code", required: true, autocomplete: "off", autofocus: true %>
7
+ </div>
8
+ </fieldset>
9
+ <fieldset class="actions">
10
+ <%= submit_tag t("active_admin.device_authorization.continue") %>
11
+ </fieldset>
12
+ <% end %>
@@ -0,0 +1,147 @@
1
+ <style>
2
+ .device-auth-standalone {
3
+ box-sizing: border-box;
4
+ color: #1a1a1a;
5
+ font-family: Georgia, "Times New Roman", serif;
6
+ line-height: 1.5;
7
+ margin: 0 auto;
8
+ max-width: 40rem;
9
+ padding: 1.5rem 1rem 3rem;
10
+ }
11
+ .device-auth-standalone *,
12
+ .device-auth-standalone *::before,
13
+ .device-auth-standalone *::after {
14
+ box-sizing: border-box;
15
+ }
16
+ .device-auth-standalone h1,
17
+ .device-auth-standalone .panel-title {
18
+ color: #111;
19
+ font-family: Georgia, "Times New Roman", serif;
20
+ font-size: 1.75rem;
21
+ font-weight: 700;
22
+ line-height: 1.25;
23
+ margin: 0 0 1rem;
24
+ }
25
+ .device-auth-standalone .panel {
26
+ background: #fff;
27
+ border: 1px solid #c8c8c8;
28
+ margin: 0 0 1.25rem;
29
+ padding: 1rem 1.1rem;
30
+ }
31
+ .device-auth-standalone .panel-body > *:first-child {
32
+ margin-top: 0;
33
+ }
34
+ .device-auth-standalone .panel-body > *:last-child {
35
+ margin-bottom: 0;
36
+ }
37
+ .device-auth-standalone .attributes_table {
38
+ border-collapse: collapse;
39
+ margin: 0 0 1rem;
40
+ width: 100%;
41
+ }
42
+ .device-auth-standalone .attributes_table th,
43
+ .device-auth-standalone .attributes_table td {
44
+ border-bottom: 1px solid #ddd;
45
+ padding: 0.55rem 0.35rem;
46
+ text-align: left;
47
+ vertical-align: top;
48
+ }
49
+ .device-auth-standalone .attributes_table th {
50
+ color: #444;
51
+ font-weight: 600;
52
+ width: 8.5rem;
53
+ }
54
+ .device-auth-standalone .device-auth-user-code {
55
+ font-family: ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas, monospace;
56
+ font-size: 1.15rem;
57
+ letter-spacing: 0.08em;
58
+ }
59
+ .device-auth-standalone .flash {
60
+ border: 1px solid #8a2f1d;
61
+ border-radius: 2px;
62
+ margin: 0 0 1rem;
63
+ padding: 0.75rem 0.9rem;
64
+ }
65
+ .device-auth-standalone .flash_alert {
66
+ background: #f8e8e4;
67
+ color: #5c1f14;
68
+ }
69
+ .device-auth-standalone .inline-hints {
70
+ color: #555;
71
+ margin: 0 0 1rem;
72
+ }
73
+ .device-auth-standalone fieldset {
74
+ border: 0;
75
+ margin: 0 0 1rem;
76
+ min-width: 0;
77
+ padding: 0;
78
+ }
79
+ .device-auth-standalone legend,
80
+ .device-auth-standalone .label {
81
+ color: #222;
82
+ display: block;
83
+ font-weight: 600;
84
+ margin: 0 0 0.4rem;
85
+ }
86
+ .device-auth-standalone .choice {
87
+ list-style: none;
88
+ margin: 0 0 0.45rem;
89
+ }
90
+ .device-auth-standalone .choice label {
91
+ display: inline-flex;
92
+ gap: 0.45rem;
93
+ align-items: flex-start;
94
+ }
95
+ .device-auth-standalone input[type="text"],
96
+ .device-auth-standalone select {
97
+ border: 1px solid #888;
98
+ border-radius: 2px;
99
+ display: block;
100
+ font: inherit;
101
+ max-width: 100%;
102
+ padding: 0.5rem 0.6rem;
103
+ width: 100%;
104
+ }
105
+ .device-auth-standalone input[type="text"]:focus,
106
+ .device-auth-standalone select:focus,
107
+ .device-auth-standalone input[type="checkbox"]:focus,
108
+ .device-auth-standalone input[type="submit"]:focus {
109
+ outline: 3px solid #1d4f91;
110
+ outline-offset: 2px;
111
+ }
112
+ .device-auth-standalone .device-auth-actions ol {
113
+ display: flex;
114
+ flex-direction: column;
115
+ gap: 0.65rem;
116
+ list-style: none;
117
+ margin: 0;
118
+ padding: 0;
119
+ }
120
+ .device-auth-standalone .device-auth-actions input[type="submit"],
121
+ .device-auth-standalone fieldset.actions input[type="submit"] {
122
+ background: #1d4f91;
123
+ border: 1px solid #163d70;
124
+ border-radius: 2px;
125
+ color: #fff;
126
+ cursor: pointer;
127
+ font: inherit;
128
+ min-height: 2.5rem;
129
+ padding: 0.45rem 1rem;
130
+ width: 100%;
131
+ }
132
+ .device-auth-standalone .device-auth-actions li:last-child input[type="submit"] {
133
+ background: #fff;
134
+ border-color: #888;
135
+ color: #222;
136
+ }
137
+ @media (min-width: 40rem) {
138
+ .device-auth-standalone .device-auth-actions ol {
139
+ flex-direction: row;
140
+ flex-wrap: wrap;
141
+ }
142
+ .device-auth-standalone .device-auth-actions input[type="submit"],
143
+ .device-auth-standalone fieldset.actions input[type="submit"] {
144
+ width: auto;
145
+ }
146
+ }
147
+ </style>
@@ -0,0 +1,19 @@
1
+ <!DOCTYPE html>
2
+ <html lang="<%= I18n.locale %>">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title><%= t("active_admin.device_authorization.enter_code") %></title>
7
+ <%= render partial: "active_admin/device_authorization/standalone_styles" %>
8
+ </head>
9
+ <body>
10
+ <main class="device-auth-standalone">
11
+ <h1><%= t("active_admin.device_authorization.enter_code") %></h1>
12
+ <div class="panel">
13
+ <div class="panel-body">
14
+ <%= render partial: "active_admin/device_authorization/enter_code" %>
15
+ </div>
16
+ </div>
17
+ </main>
18
+ </body>
19
+ </html>
@@ -0,0 +1,14 @@
1
+ <!DOCTYPE html>
2
+ <html lang="<%= I18n.locale %>">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title><%= t("active_admin.device_authorization.title") %></title>
7
+ <%= render partial: "active_admin/device_authorization/standalone_styles" %>
8
+ </head>
9
+ <body>
10
+ <main class="device-auth-standalone">
11
+ <%= render partial: "active_admin/device_authorization/consent", locals: {grant: @grant, heading_level: :h1} %>
12
+ </main>
13
+ </body>
14
+ </html>
@@ -0,0 +1,29 @@
1
+ en:
2
+ active_admin:
3
+ device_authorization:
4
+ menu: Device authorization
5
+ title: Authorize device
6
+ enter_code: Enter the user code
7
+ enter_code_hint: Enter the code shown by the client to review its requested access.
8
+ continue: Continue
9
+ intro: A client asked for a short-lived admin access token. Review the client, scopes, and lifetime before approving.
10
+ verify_code: Verify that this code matches the code shown on the device you are using. Deny the request if the codes do not match.
11
+ client: Client
12
+ user_code: User code
13
+ status: Status
14
+ idle_note: The token also expires after %{idle_hours}h without use, and no later than the absolute lifetime you choose when absolute expiry is enabled.
15
+ scopes: Scopes
16
+ scopes_hint: Uncheck any scope this session should not receive.
17
+ absolute_ttl: Absolute lifetime
18
+ approve: Approve
19
+ deny: Deny
20
+ approved: Request approved. Return to the client; it will receive the token shortly.
21
+ denied: Request denied.
22
+ not_actionable: This request is %{status} and cannot be changed here.
23
+ error:
24
+ not_pending: This request is no longer pending.
25
+ expired: This request expired.
26
+ invalid_scope: Choose at least one valid scope.
27
+ unsupported_scope: One or more scopes are not allowed for this client.
28
+ scope_not_requested: A selected scope was not requested by the client.
29
+ resource_owner_inactive: Your account cannot approve tokens.
@@ -0,0 +1,29 @@
1
+ fi:
2
+ active_admin:
3
+ device_authorization:
4
+ menu: Laitteistovaltuutus
5
+ title: Valtuuta laite
6
+ enter_code: Syötä käyttäjäkoodi
7
+ enter_code_hint: Syötä asiakkaan näyttämä koodi ja tarkista sen pyytämät oikeudet.
8
+ continue: Jatka
9
+ intro: Asiakas pyysi lyhytikäistä admin-tunnusta. Tarkista asiakas, oikeudet ja voimassaolo ennen hyväksyntää.
10
+ verify_code: Varmista, että tämä koodi vastaa käyttämäsi laitteen näyttämää koodia. Hylkää pyyntö, jos koodit eivät täsmää.
11
+ client: Asiakas
12
+ user_code: Käyttäjäkoodi
13
+ status: Tila
14
+ idle_note: Tunnus vanhenee myös %{idle_hours} h käyttämättömyyden jälkeen ja viimeistään valitsemasi absoluuttisen ajan kuluttua, jos absoluuttinen vanheneminen on käytössä.
15
+ scopes: Oikeudet
16
+ scopes_hint: Poista valinta oikeuksista, joita tämä istunto ei saa.
17
+ absolute_ttl: Absoluuttinen voimassaolo
18
+ approve: Hyväksy
19
+ deny: Hylkää
20
+ approved: Pyyntö hyväksytty. Palaa asiakkaaseen; se vastaanottaa tunnuksen pian.
21
+ denied: Pyyntö hylätty.
22
+ not_actionable: Tämä pyyntö on tilassa %{status} eikä sitä voi muuttaa täällä.
23
+ error:
24
+ not_pending: Pyyntö ei ole enää odottava.
25
+ expired: Pyyntö vanhentui.
26
+ invalid_scope: Valitse vähintään yksi kelvollinen oikeus.
27
+ unsupported_scope: Yksi tai useampi oikeus ei ole sallittu tälle asiakkaalle.
28
+ scope_not_requested: Valittu oikeus ei ollut asiakkaan pyynnössä.
29
+ resource_owner_inactive: Tilisi ei voi hyväksyä tunnuksia.
@@ -0,0 +1,29 @@
1
+ sv:
2
+ active_admin:
3
+ device_authorization:
4
+ menu: Enhetsauktorisering
5
+ title: Auktorisera enhet
6
+ enter_code: Ange användarkoden
7
+ enter_code_hint: Ange koden som klienten visar och granska den begärda åtkomsten.
8
+ continue: Fortsätt
9
+ intro: En klient begärde en kortlivad admin-åtkomsttoken. Kontrollera klient, behörigheter och livstid innan du godkänner.
10
+ verify_code: Kontrollera att den här koden stämmer överens med koden som visas på enheten du använder. Neka begäran om koderna inte stämmer överens.
11
+ client: Klient
12
+ user_code: Användarkod
13
+ status: Status
14
+ idle_note: Token går också ut efter %{idle_hours} h utan användning, och senast efter den absoluta livstid du väljer när absolut utgång är aktiverad.
15
+ scopes: Behörigheter
16
+ scopes_hint: Avmarkera behörigheter som den här sessionen inte ska få.
17
+ absolute_ttl: Absolut livstid
18
+ approve: Godkänn
19
+ deny: Neka
20
+ approved: Begäran godkänd. Gå tillbaka till klienten; den tar emot token snart.
21
+ denied: Begäran nekad.
22
+ not_actionable: Denna begäran är %{status} och kan inte ändras här.
23
+ error:
24
+ not_pending: Begäran väntar inte längre.
25
+ expired: Begäran har gått ut.
26
+ invalid_scope: Välj minst en giltig behörighet.
27
+ unsupported_scope: En eller flera behörigheter tillåts inte för denna klient.
28
+ scope_not_requested: En vald behörighet fanns inte i klientens begäran.
29
+ resource_owner_inactive: Ditt konto kan inte godkänna tokens.
@@ -0,0 +1,8 @@
1
+ track_under:
2
+ - lib
3
+ - app
4
+ minimum_line_percent: 50
5
+ report_formats:
6
+ - html
7
+ - cobertura
8
+ - console
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module DeviceAuthorization
5
+ # Pure expiry policy for bearer access tokens.
6
+ # absolute_expires_at and last_used_at are Time-like; config TTLs are seconds or nil.
7
+ class AccessTokenExpiry
8
+ def initialize(config: ActiveAdmin::DeviceAuthorization.config)
9
+ @config = config
10
+ end
11
+
12
+ def expired?(absolute_expires_at:, last_used_at:, revoked_at:, now: Time.current)
13
+ return true if revoked_at.present?
14
+ if @config.absolute_expiry_enabled? && absolute_expires_at.present? && absolute_expires_at <= now
15
+ return true
16
+ end
17
+ if @config.idle_expiry_enabled? && last_used_at.present? &&
18
+ last_used_at <= now - @config.access_token_idle_ttl.to_i
19
+ return true
20
+ end
21
+
22
+ false
23
+ end
24
+
25
+ def expires_in_seconds(absolute_expires_at:, last_used_at:, now: Time.current)
26
+ candidates = []
27
+ if @config.absolute_expiry_enabled? && absolute_expires_at.present?
28
+ candidates << (absolute_expires_at - now).to_i
29
+ end
30
+ if @config.idle_expiry_enabled? && last_used_at.present?
31
+ candidates << (last_used_at + @config.access_token_idle_ttl.to_i - now).to_i
32
+ end
33
+ return nil if candidates.empty?
34
+
35
+ [candidates.min, 0].max
36
+ end
37
+
38
+ def absolute_expires_at_for(ttl_seconds:, now: Time.current)
39
+ return nil unless @config.absolute_expiry_enabled?
40
+
41
+ clamped = @config.clamp_absolute_ttl(ttl_seconds) || @config.access_token_default_absolute_ttl.to_i
42
+ now + clamped
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module DeviceAuthorization
5
+ module Callback
6
+ module_function
7
+
8
+ def call(callback, **arguments)
9
+ callback&.call(**arguments)
10
+ rescue => error
11
+ Rails.error.report(error, handled: true)
12
+ end
13
+ end
14
+ end
15
+ end