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 +4 -4
- data/CHANGELOG.md +6 -1
- data/README.md +29 -3
- data/activeadmin-device_authorization.gemspec +1 -1
- data/app/controllers/active_admin/device_authorization/consents_controller.rb +1 -1
- data/app/views/active_admin/device_authorization/_consent.html.erb +75 -60
- data/app/views/active_admin/device_authorization/_standalone_styles.html.erb +17 -13
- data/lib/active_admin/device_authorization/configuration.rb +19 -1
- data/lib/active_admin/device_authorization/page.rb +2 -2
- data/lib/active_admin/device_authorization/version.rb +1 -1
- metadata +9 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6edcb0a43663f2ee55e360f2571a9d980265076434205a603dbe4b41a785d351
|
|
4
|
+
data.tar.gz: 438551d1316c58ace7c9b6a81aec3f388a8a261dffdb29d40590348eb9da33e0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ae06adfc6827bd0c6be364d76716bc0771c4f0012b21a4a6c23cdee93ed395c29b02ad8c60cac0ba0c3bef4bf950767b0bc6ad9b10611da849500810a70dca39
|
|
7
|
+
data.tar.gz: da1d891cadd3eb99c7fa0391d6dfddb306baac964cc66599d932fb12ca6acd742d9ab2766d2a51466ef88e9f5d839ec3aee046f7ae995282916008c7537fddbf
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
-
##
|
|
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
|
|
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
|
-
|
|
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
|
|
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", ">=
|
|
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.
|
|
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-
|
|
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
|
-
|
|
18
|
-
margin: 0;
|
|
19
|
-
padding: 0;
|
|
25
|
+
margin-top: 0.75rem;
|
|
20
26
|
}
|
|
21
27
|
@media (min-width: 40rem) {
|
|
22
|
-
.device-auth-actions
|
|
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
|
-
<
|
|
39
|
-
<
|
|
40
|
-
<
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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="
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
<
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
<%
|
|
71
|
-
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
108
|
+
</ol>
|
|
78
109
|
</fieldset>
|
|
79
110
|
<% end %>
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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 .
|
|
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 .
|
|
43
|
-
|
|
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 .
|
|
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 .
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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!
|
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.
|
|
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:
|
|
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:
|
|
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
|