fosm-rails 0.2.4 → 0.2.5
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/app/controllers/fosm/admin/roles_controller.rb +1 -1
- data/app/views/fosm/admin/roles/new.html.erb +123 -13
- data/app/views/layouts/fosm/application.html.erb +1 -0
- data/lib/fosm/engine.rb +12 -13
- data/lib/fosm/registry.rb +22 -0
- data/lib/fosm/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6f80bae4ce6fd6dba16419a7a68ac6cec93439bb0296caf5e3c335f039e216ab
|
|
4
|
+
data.tar.gz: 468c55584847418dc48a49466bd1b13fba8268deb40c20086bce383f8b7221ba
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 988a291c82244fd9249e4698df06f921e0a0eddfb20811238dd42b6d780a90560a3aaded7027260b79e7a4d6e31d5489c877d9b7ea0567ba4bd5e65eea95644c
|
|
7
|
+
data.tar.gz: 2a294a01dd336497e1437a536c8b2e6b1ca69bcffaaea6240e05572e1c9e632ae599c4abd2e099cafb98e8d91c3786fd9a377d9805f43feadef296bbccc1fbc1
|
|
@@ -122,12 +122,21 @@
|
|
|
122
122
|
})();
|
|
123
123
|
</script>
|
|
124
124
|
|
|
125
|
+
<%# Build a JSON map: { "Fosm::PartnershipAgreement" => ["owner","viewer"], ... }
|
|
126
|
+
Used by the JS below to filter Role Name options when resource type changes. %>
|
|
127
|
+
<% roles_by_type = @apps.each_with_object({}) { |(_, klass), h|
|
|
128
|
+
next unless klass.fosm_lifecycle&.access_defined?
|
|
129
|
+
h[klass.name] = klass.fosm_lifecycle.access_definition.role_names.map(&:to_s).sort
|
|
130
|
+
} %>
|
|
131
|
+
|
|
125
132
|
<div>
|
|
126
133
|
<label class="block text-sm font-medium text-gray-700 mb-1">Resource Type</label>
|
|
127
134
|
<%= f.select :resource_type,
|
|
128
135
|
@apps.map { |_, klass| [klass.name.demodulize, klass.name] },
|
|
129
136
|
{ include_blank: "— Select a FOSM app —" },
|
|
130
|
-
|
|
137
|
+
id: "role_assignment_resource_type",
|
|
138
|
+
class: "w-full border border-gray-300 rounded px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-blue-500",
|
|
139
|
+
data: { roles: roles_by_type.to_json } %>
|
|
131
140
|
</div>
|
|
132
141
|
|
|
133
142
|
<div>
|
|
@@ -141,29 +150,130 @@
|
|
|
141
150
|
</p>
|
|
142
151
|
</div>
|
|
143
152
|
|
|
144
|
-
<div>
|
|
153
|
+
<div id="role-name-field">
|
|
145
154
|
<label class="block text-sm font-medium text-gray-700 mb-1">Role Name</label>
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
["#{klass.name.demodulize} → :#{rn}", rn.to_s]
|
|
150
|
-
}
|
|
151
|
-
}.uniq(&:last) %>
|
|
152
|
-
<% if role_options.any? %>
|
|
155
|
+
<%# Initial render: all known roles sorted. JS narrows this on resource type change. %>
|
|
156
|
+
<% all_roles = roles_by_type.values.flatten.uniq.sort %>
|
|
157
|
+
<% if all_roles.any? %>
|
|
153
158
|
<%= f.select :role_name,
|
|
154
|
-
|
|
159
|
+
all_roles.map { |rn| [":#{rn}", rn] },
|
|
155
160
|
{ include_blank: "— Select a role —" },
|
|
161
|
+
id: "role_assignment_role_name",
|
|
156
162
|
class: "w-full border border-gray-300 rounded px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-blue-500" %>
|
|
157
163
|
<% else %>
|
|
158
|
-
<%= f.
|
|
159
|
-
|
|
164
|
+
<%= f.select :role_name,
|
|
165
|
+
[],
|
|
166
|
+
{ include_blank: "— Select a role —" },
|
|
167
|
+
id: "role_assignment_role_name",
|
|
160
168
|
class: "w-full border border-gray-300 rounded px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-blue-500" %>
|
|
161
169
|
<% end %>
|
|
162
|
-
<p class="text-xs text-gray-400 mt-1"
|
|
170
|
+
<p class="text-xs text-gray-400 mt-1" id="role-name-hint">
|
|
171
|
+
Must match a role declared in the lifecycle <code>access</code> block.
|
|
172
|
+
</p>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
<div id="no-rbac-warning" class="hidden rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800">
|
|
176
|
+
<strong>No RBAC on this resource type.</strong>
|
|
177
|
+
<span id="no-rbac-warning-name"></span> has no <code>access</code> block in its lifecycle —
|
|
178
|
+
all authenticated users can already fire any transition.
|
|
179
|
+
Role assignments for this type would be stored but never enforced.
|
|
163
180
|
</div>
|
|
164
181
|
|
|
182
|
+
<script>
|
|
183
|
+
(function() {
|
|
184
|
+
var resourceSelect = document.getElementById('role_assignment_resource_type');
|
|
185
|
+
var roleField = document.getElementById('role-name-field');
|
|
186
|
+
var roleHint = document.getElementById('role-name-hint');
|
|
187
|
+
var warning = document.getElementById('no-rbac-warning');
|
|
188
|
+
var warningName = document.getElementById('no-rbac-warning-name');
|
|
189
|
+
var rolesByType = JSON.parse(resourceSelect.dataset.roles || '{}');
|
|
190
|
+
|
|
191
|
+
// Looked up lazily — the submit button appears after this script tag in the DOM.
|
|
192
|
+
function submitBtn() { return document.getElementById('role-grant-submit'); }
|
|
193
|
+
|
|
194
|
+
function currentRoleInput() {
|
|
195
|
+
return document.getElementById('role_assignment_role_name');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function allRoles() {
|
|
199
|
+
return Object.values(rolesByType).flat()
|
|
200
|
+
.filter(function(v, i, a) { return a.indexOf(v) === i; })
|
|
201
|
+
.sort();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function buildSelect(roles, inputName, inputClass) {
|
|
205
|
+
var sel = document.createElement('select');
|
|
206
|
+
sel.id = 'role_assignment_role_name';
|
|
207
|
+
sel.name = inputName;
|
|
208
|
+
sel.className = inputClass;
|
|
209
|
+
var blank = document.createElement('option');
|
|
210
|
+
blank.value = '';
|
|
211
|
+
blank.textContent = '— Select a role —';
|
|
212
|
+
sel.appendChild(blank);
|
|
213
|
+
roles.forEach(function(rn) {
|
|
214
|
+
var opt = document.createElement('option');
|
|
215
|
+
opt.value = rn;
|
|
216
|
+
opt.textContent = ':' + rn;
|
|
217
|
+
sel.appendChild(opt);
|
|
218
|
+
});
|
|
219
|
+
return sel;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function replaceInput(newEl) {
|
|
223
|
+
var old = currentRoleInput();
|
|
224
|
+
old.parentNode.replaceChild(newEl, old);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function setNoRbac(typeName) {
|
|
228
|
+
roleField.classList.add('hidden');
|
|
229
|
+
warning.classList.remove('hidden');
|
|
230
|
+
warningName.textContent = ' ' + typeName;
|
|
231
|
+
var btn = submitBtn();
|
|
232
|
+
if (btn) {
|
|
233
|
+
btn.disabled = true;
|
|
234
|
+
btn.classList.add('opacity-50', 'cursor-not-allowed');
|
|
235
|
+
btn.classList.remove('hover:bg-gray-700', 'cursor-pointer');
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function clearNoRbac() {
|
|
240
|
+
roleField.classList.remove('hidden');
|
|
241
|
+
warning.classList.add('hidden');
|
|
242
|
+
warningName.textContent = '';
|
|
243
|
+
var btn = submitBtn();
|
|
244
|
+
if (btn) {
|
|
245
|
+
btn.disabled = false;
|
|
246
|
+
btn.classList.remove('opacity-50', 'cursor-not-allowed');
|
|
247
|
+
btn.classList.add('hover:bg-gray-700', 'cursor-pointer');
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
resourceSelect.addEventListener('change', function() {
|
|
252
|
+
var selectedType = resourceSelect.value;
|
|
253
|
+
var old = currentRoleInput();
|
|
254
|
+
var inputName = old.name;
|
|
255
|
+
var inputClass = old.className;
|
|
256
|
+
var roles = rolesByType[selectedType];
|
|
257
|
+
|
|
258
|
+
if (!selectedType) {
|
|
259
|
+
clearNoRbac();
|
|
260
|
+
replaceInput(buildSelect(allRoles(), inputName, inputClass));
|
|
261
|
+
roleHint.innerHTML = 'Must match a role declared in the lifecycle <code>access</code> block.';
|
|
262
|
+
} else if (roles && roles.length > 0) {
|
|
263
|
+
clearNoRbac();
|
|
264
|
+
replaceInput(buildSelect(roles, inputName, inputClass));
|
|
265
|
+
roleHint.textContent = roles.length + ' role' + (roles.length !== 1 ? 's' : '') +
|
|
266
|
+
' available for ' + selectedType.split('::').pop() + '.';
|
|
267
|
+
} else {
|
|
268
|
+
setNoRbac(selectedType.split('::').pop());
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
})();
|
|
272
|
+
</script>
|
|
273
|
+
|
|
165
274
|
<div class="flex items-center gap-3 pt-2">
|
|
166
275
|
<%= f.submit "Grant Role",
|
|
276
|
+
id: "role-grant-submit",
|
|
167
277
|
class: "bg-gray-900 text-white text-sm px-4 py-2 rounded hover:bg-gray-700 cursor-pointer" %>
|
|
168
278
|
<%= link_to "Cancel", fosm.admin_roles_path,
|
|
169
279
|
class: "text-sm text-gray-500 hover:underline" %>
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
<nav class="p-3 space-y-1 flex-1">
|
|
20
20
|
<%= link_to "Dashboard", fosm.admin_root_path, class: "block px-3 py-2 rounded text-sm text-gray-700 hover:bg-gray-100" %>
|
|
21
21
|
<%= link_to "Transitions", fosm.admin_transitions_path, class: "block px-3 py-2 rounded text-sm text-gray-700 hover:bg-gray-100" %>
|
|
22
|
+
<%= link_to "Roles", fosm.admin_roles_path, class: "block px-3 py-2 rounded text-sm text-gray-700 hover:bg-gray-100" %>
|
|
22
23
|
<%= link_to "Webhooks", fosm.admin_webhooks_path, class: "block px-3 py-2 rounded text-sm text-gray-700 hover:bg-gray-100" %>
|
|
23
24
|
</nav>
|
|
24
25
|
<div class="p-3 border-t border-gray-100 space-y-1">
|
data/lib/fosm/engine.rb
CHANGED
|
@@ -114,23 +114,22 @@ module Fosm
|
|
|
114
114
|
end
|
|
115
115
|
end
|
|
116
116
|
|
|
117
|
-
#
|
|
117
|
+
# Re-register all Fosm models after every code reload (development) and
|
|
118
|
+
# once at boot (all environments). to_prepare fires on every reload in
|
|
119
|
+
# development, and once in production/test — making the registry always
|
|
120
|
+
# consistent with the currently-loaded class objects.
|
|
118
121
|
# Use ::Rails to avoid ambiguity with Fosm::Rails module.
|
|
119
|
-
config.
|
|
122
|
+
config.to_prepare do
|
|
120
123
|
::Rails.application.eager_load! if ::Rails.env.development? && !::Rails.application.config.eager_load
|
|
121
124
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
klass.respond_to?(:fosm_lifecycle) &&
|
|
126
|
-
klass.fosm_lifecycle.present?
|
|
127
|
-
}.each do |klass|
|
|
128
|
-
slug = klass.name.demodulize.underscore
|
|
129
|
-
Fosm::Registry.register(klass, slug: slug)
|
|
130
|
-
end
|
|
125
|
+
Fosm::Registry.clear!
|
|
126
|
+
Fosm::Registry.repopulate!
|
|
127
|
+
end
|
|
131
128
|
|
|
132
|
-
|
|
133
|
-
|
|
129
|
+
# Start the background flusher thread for the :buffered log strategy.
|
|
130
|
+
# Kept in after_initialize (runs once) so the thread is not re-spawned on
|
|
131
|
+
# every development reload.
|
|
132
|
+
config.after_initialize do
|
|
134
133
|
if Fosm.config.transition_log_strategy == :buffered && !::Rails.env.test?
|
|
135
134
|
Fosm::TransitionBuffer.start_flusher!
|
|
136
135
|
end
|
data/lib/fosm/registry.rb
CHANGED
|
@@ -39,6 +39,28 @@ module Fosm
|
|
|
39
39
|
def each(&block)
|
|
40
40
|
@registered.each(&block)
|
|
41
41
|
end
|
|
42
|
+
|
|
43
|
+
# Remove all registered entries.
|
|
44
|
+
# Called by to_prepare in development so stale class references are
|
|
45
|
+
# replaced after Rails reloads the application code.
|
|
46
|
+
def clear!
|
|
47
|
+
@registered = {}
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Scan ObjectSpace for all ActiveRecord subclasses that include
|
|
51
|
+
# Fosm::Lifecycle and register them. Calling this after clear! is
|
|
52
|
+
# equivalent to the boot-time registration that after_initialize performs.
|
|
53
|
+
def repopulate!
|
|
54
|
+
ObjectSpace.each_object(Class).select { |klass|
|
|
55
|
+
klass < ActiveRecord::Base &&
|
|
56
|
+
klass.name&.start_with?("Fosm::") &&
|
|
57
|
+
klass.respond_to?(:fosm_lifecycle) &&
|
|
58
|
+
klass.fosm_lifecycle.present?
|
|
59
|
+
}.each do |klass|
|
|
60
|
+
slug = klass.name.demodulize.underscore
|
|
61
|
+
register(klass, slug: slug)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
42
64
|
end
|
|
43
65
|
end
|
|
44
66
|
end
|
data/lib/fosm/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fosm-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Abhishek Parolkar
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-05-
|
|
11
|
+
date: 2026-05-07 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|