permission_forge 0.14.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.
Files changed (406) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +1138 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +356 -0
  5. data/app/controllers/concerns/permission_forge/acting_admin.rb +17 -0
  6. data/app/controllers/concerns/permission_forge/api/crud_actions.rb +84 -0
  7. data/app/controllers/concerns/permission_forge/api/nested_resource_lookup.rb +18 -0
  8. data/app/controllers/concerns/permission_forge/api/pagination.rb +50 -0
  9. data/app/controllers/concerns/permission_forge/api/response_renderer.rb +65 -0
  10. data/app/controllers/concerns/permission_forge/api/serializer_resolver.rb +30 -0
  11. data/app/controllers/concerns/permission_forge/api_authentication.rb +40 -0
  12. data/app/controllers/concerns/permission_forge/assignment_authorization.rb +97 -0
  13. data/app/controllers/concerns/permission_forge/audit_log_gate.rb +55 -0
  14. data/app/controllers/concerns/permission_forge/basic_authentication.rb +77 -0
  15. data/app/controllers/concerns/permission_forge/cache_confirmation.rb +32 -0
  16. data/app/controllers/concerns/permission_forge/forgery_protection.rb +29 -0
  17. data/app/controllers/concerns/permission_forge/html_format.rb +40 -0
  18. data/app/controllers/concerns/permission_forge/param_shapes.rb +87 -0
  19. data/app/controllers/concerns/permission_forge/tenant_required.rb +42 -0
  20. data/app/controllers/permission_forge/api/audit_events_controller.rb +33 -0
  21. data/app/controllers/permission_forge/api/base_controller.rb +220 -0
  22. data/app/controllers/permission_forge/api/dashboard_controller.rb +30 -0
  23. data/app/controllers/permission_forge/api/permission_set_group_users_controller.rb +51 -0
  24. data/app/controllers/permission_forge/api/permission_set_groups_controller.rb +71 -0
  25. data/app/controllers/permission_forge/api/permission_set_rules_controller.rb +37 -0
  26. data/app/controllers/permission_forge/api/permission_set_users_controller.rb +105 -0
  27. data/app/controllers/permission_forge/api/permission_sets_controller.rb +87 -0
  28. data/app/controllers/permission_forge/api/resource_actions_controller.rb +58 -0
  29. data/app/controllers/permission_forge/api/resource_fields_controller.rb +30 -0
  30. data/app/controllers/permission_forge/api/resources_controller.rb +56 -0
  31. data/app/controllers/permission_forge/application_controller.rb +198 -0
  32. data/app/controllers/permission_forge/assets_controller.rb +97 -0
  33. data/app/controllers/permission_forge/assignments_controller.rb +266 -0
  34. data/app/controllers/permission_forge/audit_events_controller.rb +94 -0
  35. data/app/controllers/permission_forge/bulk_assignments_controller.rb +108 -0
  36. data/app/controllers/permission_forge/field_lookups_controller.rb +38 -0
  37. data/app/controllers/permission_forge/home_controller.rb +24 -0
  38. data/app/controllers/permission_forge/maintenance_controller.rb +13 -0
  39. data/app/controllers/permission_forge/people_controller.rb +190 -0
  40. data/app/controllers/permission_forge/permission_set_editor.rb +71 -0
  41. data/app/controllers/permission_forge/permission_set_group_users_controller.rb +57 -0
  42. data/app/controllers/permission_forge/permission_set_groups_controller.rb +111 -0
  43. data/app/controllers/permission_forge/permission_set_rules_controller.rb +97 -0
  44. data/app/controllers/permission_forge/permission_sets_controller.rb +227 -0
  45. data/app/controllers/permission_forge/preferences_controller.rb +26 -0
  46. data/app/controllers/permission_forge/resource_actions_controller.rb +106 -0
  47. data/app/controllers/permission_forge/resource_fields_controller.rb +129 -0
  48. data/app/controllers/permission_forge/resources_controller.rb +189 -0
  49. data/app/controllers/permission_forge/settings_controller.rb +33 -0
  50. data/app/frontend/permission_forge/brand/apple-touch-icon.png +0 -0
  51. data/app/frontend/permission_forge/brand/chevron.svg +1 -0
  52. data/app/frontend/permission_forge/brand/favicon.ico +0 -0
  53. data/app/frontend/permission_forge/brand/favicon.svg +1 -0
  54. data/app/frontend/permission_forge/brand/logo-horizontal-dark.svg +1 -0
  55. data/app/frontend/permission_forge/brand/logo-horizontal-light.svg +1 -0
  56. data/app/frontend/permission_forge/brand/mark-small.svg +1 -0
  57. data/app/frontend/permission_forge/brand/mark.svg +1 -0
  58. data/app/frontend/permission_forge/pf_admin.css +766 -0
  59. data/app/frontend/permission_forge/pf_admin.js +482 -0
  60. data/app/helpers/permission_forge/application_helper.rb +167 -0
  61. data/app/helpers/permission_forge/audit_helper.rb +230 -0
  62. data/app/helpers/permission_forge/audit_sentences_helper.rb +84 -0
  63. data/app/helpers/permission_forge/badges_helper.rb +134 -0
  64. data/app/helpers/permission_forge/changes_helper.rb +117 -0
  65. data/app/helpers/permission_forge/people_helper.rb +118 -0
  66. data/app/helpers/permission_forge/sentences_helper.rb +175 -0
  67. data/app/helpers/permission_forge/settings_helper.rb +152 -0
  68. data/app/helpers/permission_forge/ui_helper.rb +184 -0
  69. data/app/models/permission_forge/audit_event.rb +159 -0
  70. data/app/models/permission_forge/permission_set.rb +100 -0
  71. data/app/models/permission_forge/permission_set_group.rb +83 -0
  72. data/app/models/permission_forge/permission_set_group_user.rb +43 -0
  73. data/app/models/permission_forge/permission_set_rule.rb +99 -0
  74. data/app/models/permission_forge/permission_set_user.rb +158 -0
  75. data/app/models/permission_forge/resource.rb +100 -0
  76. data/app/models/permission_forge/resource_action.rb +58 -0
  77. data/app/models/permission_forge/resource_field.rb +69 -0
  78. data/app/views/layouts/permission_forge/application.html.erb +30 -0
  79. data/app/views/layouts/permission_forge/fragment.html.erb +2 -0
  80. data/app/views/permission_forge/assignments/_person_options.html.erb +26 -0
  81. data/app/views/permission_forge/assignments/confirm_destroy.html.erb +24 -0
  82. data/app/views/permission_forge/assignments/new.html.erb +99 -0
  83. data/app/views/permission_forge/audit_events/index.html.erb +161 -0
  84. data/app/views/permission_forge/audit_events/show.html.erb +146 -0
  85. data/app/views/permission_forge/bulk_assignments/confirm_revoke.html.erb +21 -0
  86. data/app/views/permission_forge/bulk_assignments/new.html.erb +69 -0
  87. data/app/views/permission_forge/field_lookups/show.html.erb +32 -0
  88. data/app/views/permission_forge/home/_audit_card.html.erb +19 -0
  89. data/app/views/permission_forge/home/_ending_card.html.erb +32 -0
  90. data/app/views/permission_forge/home/_expired_card.html.erb +34 -0
  91. data/app/views/permission_forge/home/_privacy_card.html.erb +67 -0
  92. data/app/views/permission_forge/home/_rules_card.html.erb +23 -0
  93. data/app/views/permission_forge/home/_stale_card.html.erb +39 -0
  94. data/app/views/permission_forge/home/_tidy_card.html.erb +18 -0
  95. data/app/views/permission_forge/home/index.html.erb +69 -0
  96. data/app/views/permission_forge/people/_access_tab.html.erb +58 -0
  97. data/app/views/permission_forge/people/_audit_tab.html.erb +93 -0
  98. data/app/views/permission_forge/people/_effective_field_source.html.erb +11 -0
  99. data/app/views/permission_forge/people/_effective_tab.html.erb +118 -0
  100. data/app/views/permission_forge/people/_effective_why.html.erb +48 -0
  101. data/app/views/permission_forge/people/_why_item.html.erb +19 -0
  102. data/app/views/permission_forge/people/confirm_remove_all.html.erb +27 -0
  103. data/app/views/permission_forge/people/consent_preview.html.erb +39 -0
  104. data/app/views/permission_forge/people/erasure_preview.html.erb +28 -0
  105. data/app/views/permission_forge/people/index.html.erb +78 -0
  106. data/app/views/permission_forge/people/show.html.erb +54 -0
  107. data/app/views/permission_forge/permission_set_groups/confirm_destroy.html.erb +12 -0
  108. data/app/views/permission_forge/permission_set_groups/index.html.erb +55 -0
  109. data/app/views/permission_forge/permission_set_groups/show.html.erb +100 -0
  110. data/app/views/permission_forge/permission_set_rules/_builder.html.erb +145 -0
  111. data/app/views/permission_forge/permission_set_rules/_value_input.html.erb +42 -0
  112. data/app/views/permission_forge/permission_set_rules/confirm_destroy.html.erb +13 -0
  113. data/app/views/permission_forge/permission_set_rules/edit.html.erb +3 -0
  114. data/app/views/permission_forge/permission_set_rules/new.html.erb +3 -0
  115. data/app/views/permission_forge/permission_sets/_assignments_tab.html.erb +74 -0
  116. data/app/views/permission_forge/permission_sets/_audit_tab.html.erb +80 -0
  117. data/app/views/permission_forge/permission_sets/_conditions_tab.html.erb +83 -0
  118. data/app/views/permission_forge/permission_sets/_header.html.erb +36 -0
  119. data/app/views/permission_forge/permission_sets/_records_summary.html.erb +16 -0
  120. data/app/views/permission_forge/permission_sets/_resource_summary.html.erb +11 -0
  121. data/app/views/permission_forge/permission_sets/_resources_tab.html.erb +49 -0
  122. data/app/views/permission_forge/permission_sets/confirm_destroy.html.erb +23 -0
  123. data/app/views/permission_forge/permission_sets/confirm_switch_mode.html.erb +15 -0
  124. data/app/views/permission_forge/permission_sets/index.html.erb +83 -0
  125. data/app/views/permission_forge/permission_sets/new.html.erb +63 -0
  126. data/app/views/permission_forge/permission_sets/review.html.erb +26 -0
  127. data/app/views/permission_forge/permission_sets/show.html.erb +50 -0
  128. data/app/views/permission_forge/resource_actions/_across_sets.html.erb +24 -0
  129. data/app/views/permission_forge/resource_actions/_add_select.html.erb +12 -0
  130. data/app/views/permission_forge/resource_actions/_effect.html.erb +25 -0
  131. data/app/views/permission_forge/resource_actions/_endpoints.html.erb +16 -0
  132. data/app/views/permission_forge/resource_actions/_form.html.erb +44 -0
  133. data/app/views/permission_forge/resource_actions/_managed_note.html.erb +11 -0
  134. data/app/views/permission_forge/resource_actions/confirm_destroy.html.erb +13 -0
  135. data/app/views/permission_forge/resource_actions/new.html.erb +31 -0
  136. data/app/views/permission_forge/resource_actions/show.html.erb +39 -0
  137. data/app/views/permission_forge/resource_fields/_explain.html.erb +24 -0
  138. data/app/views/permission_forge/resource_fields/show.html.erb +186 -0
  139. data/app/views/permission_forge/resources/_actions_tab.html.erb +88 -0
  140. data/app/views/permission_forge/resources/_fields_tab.html.erb +89 -0
  141. data/app/views/permission_forge/resources/_rules_tab.html.erb +55 -0
  142. data/app/views/permission_forge/resources/_sync.html.erb +57 -0
  143. data/app/views/permission_forge/resources/confirm_destroy.html.erb +17 -0
  144. data/app/views/permission_forge/resources/new.html.erb +57 -0
  145. data/app/views/permission_forge/resources/show.html.erb +117 -0
  146. data/app/views/permission_forge/settings/_ladder.html.erb +15 -0
  147. data/app/views/permission_forge/settings/_row.html.erb +38 -0
  148. data/app/views/permission_forge/settings/show.html.erb +67 -0
  149. data/app/views/permission_forge/shared/_across_sets.html.erb +31 -0
  150. data/app/views/permission_forge/shared/_audit_events.html.erb +36 -0
  151. data/app/views/permission_forge/shared/_audit_footer.html.erb +16 -0
  152. data/app/views/permission_forge/shared/_audit_off_note.html.erb +15 -0
  153. data/app/views/permission_forge/shared/_audit_pagination.html.erb +19 -0
  154. data/app/views/permission_forge/shared/_audit_refused_note.html.erb +7 -0
  155. data/app/views/permission_forge/shared/_audit_timeline.html.erb +29 -0
  156. data/app/views/permission_forge/shared/_crud_cell.html.erb +18 -0
  157. data/app/views/permission_forge/shared/_dialog_frame.html.erb +11 -0
  158. data/app/views/permission_forge/shared/_expiry_picker.html.erb +27 -0
  159. data/app/views/permission_forge/shared/_head_extra.html.erb +3 -0
  160. data/app/views/permission_forge/shared/_help_banner.html.erb +10 -0
  161. data/app/views/permission_forge/shared/_level_control.html.erb +18 -0
  162. data/app/views/permission_forge/shared/_mode_banners.html.erb +22 -0
  163. data/app/views/permission_forge/shared/_nav.html.erb +12 -0
  164. data/app/views/permission_forge/shared/_preferences.html.erb +24 -0
  165. data/app/views/permission_forge/shared/_reason_field.html.erb +20 -0
  166. data/app/views/permission_forge/shared/_savebar.html.erb +13 -0
  167. data/app/views/permission_forge/shared/_sheet_frame.html.erb +22 -0
  168. data/app/views/permission_forge/shared/_sidebar.html.erb +18 -0
  169. data/app/views/permission_forge/shared/_tech_details.html.erb +20 -0
  170. data/app/views/permission_forge/shared/_toasts.html.erb +27 -0
  171. data/app/views/permission_forge/shared/_topbar.html.erb +15 -0
  172. data/app/views/permission_forge/shared/_user_select.html.erb +16 -0
  173. data/app/views/permission_forge/shared/audit_refused.html.erb +7 -0
  174. data/config/locales/permission_forge.en.yml +2115 -0
  175. data/config/locales/permission_forge.mk.yml +2166 -0
  176. data/config/locales/permission_forge.sq.yml +2164 -0
  177. data/config/routes.rb +143 -0
  178. data/lib/generators/permission_forge/install/install_generator.rb +105 -0
  179. data/lib/generators/permission_forge/install/templates/initializer.rb.tt +148 -0
  180. data/lib/generators/permission_forge/install/templates/migration.rb.tt +198 -0
  181. data/lib/generators/permission_forge/rls/rls_generator.rb +57 -0
  182. data/lib/generators/permission_forge/rls/templates/enable_permission_forge_rls.rb.tt +15 -0
  183. data/lib/generators/permission_forge/upgrade/templates/add_admin_ui_columns.rb.tt +8 -0
  184. data/lib/generators/permission_forge/upgrade/templates/add_audit_events.rb.tt +98 -0
  185. data/lib/generators/permission_forge/upgrade/templates/add_resource_fields_unique_index.rb.tt +69 -0
  186. data/lib/generators/permission_forge/upgrade/upgrade_generator.rb +66 -0
  187. data/lib/permission_forge/admin/action_across_sets.rb +76 -0
  188. data/lib/permission_forge/admin/action_rows.rb +156 -0
  189. data/lib/permission_forge/admin/attention_items.rb +207 -0
  190. data/lib/permission_forge/admin/audit_event_detail.rb +75 -0
  191. data/lib/permission_forge/admin/audit_log.rb +193 -0
  192. data/lib/permission_forge/admin/audit_log_days.rb +41 -0
  193. data/lib/permission_forge/admin/audit_lookups.rb +105 -0
  194. data/lib/permission_forge/admin/audit_name_history.rb +85 -0
  195. data/lib/permission_forge/admin/audit_trail.rb +169 -0
  196. data/lib/permission_forge/admin/bulk_people_matcher.rb +109 -0
  197. data/lib/permission_forge/admin/change_set.rb +123 -0
  198. data/lib/permission_forge/admin/change_set_parts/actions.rb +27 -0
  199. data/lib/permission_forge/admin/change_set_parts/base.rb +58 -0
  200. data/lib/permission_forge/admin/change_set_parts/context.rb +50 -0
  201. data/lib/permission_forge/admin/change_set_parts/field_edit.rb +67 -0
  202. data/lib/permission_forge/admin/change_set_parts/field_levels.rb +71 -0
  203. data/lib/permission_forge/admin/change_set_parts/new_actions.rb +91 -0
  204. data/lib/permission_forge/admin/change_set_parts/new_fields.rb +63 -0
  205. data/lib/permission_forge/admin/change_set_parts/resource_edit.rb +55 -0
  206. data/lib/permission_forge/admin/change_set_parts/resource_flags.rb +19 -0
  207. data/lib/permission_forge/admin/change_set_parts/set_attrs.rb +45 -0
  208. data/lib/permission_forge/admin/change_set_parts/values.rb +20 -0
  209. data/lib/permission_forge/admin/column_fields.rb +48 -0
  210. data/lib/permission_forge/admin/conditions_form.rb +78 -0
  211. data/lib/permission_forge/admin/controller_catalog.rb +166 -0
  212. data/lib/permission_forge/admin/csv.rb +41 -0
  213. data/lib/permission_forge/admin/editable_lock.rb +157 -0
  214. data/lib/permission_forge/admin/effective_access.rb +158 -0
  215. data/lib/permission_forge/admin/effective_fields.rb +176 -0
  216. data/lib/permission_forge/admin/expiry_presets.rb +51 -0
  217. data/lib/permission_forge/admin/exports/document.rb +89 -0
  218. data/lib/permission_forge/admin/exports/events.rb +51 -0
  219. data/lib/permission_forge/admin/exports/holdings.rb +118 -0
  220. data/lib/permission_forge/admin/exports.rb +37 -0
  221. data/lib/permission_forge/admin/field_across_sets.rb +51 -0
  222. data/lib/permission_forge/admin/field_detail.rb +112 -0
  223. data/lib/permission_forge/admin/field_level.rb +74 -0
  224. data/lib/permission_forge/admin/field_rows.rb +137 -0
  225. data/lib/permission_forge/admin/last_changes.rb +94 -0
  226. data/lib/permission_forge/admin/model_catalog.rb +216 -0
  227. data/lib/permission_forge/admin/people_candidates.rb +108 -0
  228. data/lib/permission_forge/admin/people_directory.rb +158 -0
  229. data/lib/permission_forge/admin/permission_set_factory.rb +48 -0
  230. data/lib/permission_forge/admin/person_label.rb +95 -0
  231. data/lib/permission_forge/admin/record_id.rb +19 -0
  232. data/lib/permission_forge/admin/rule_builder.rb +58 -0
  233. data/lib/permission_forge/admin/rule_form.rb +175 -0
  234. data/lib/permission_forge/admin/rule_form_input.rb +74 -0
  235. data/lib/permission_forge/admin/sensitive_columns.rb +68 -0
  236. data/lib/permission_forge/admin/settings_catalog.rb +261 -0
  237. data/lib/permission_forge/admin/settings_values.rb +130 -0
  238. data/lib/permission_forge/api/serializers/audit_event_serializer.rb +27 -0
  239. data/lib/permission_forge/api/serializers/base_serializer.rb +36 -0
  240. data/lib/permission_forge/api/serializers/permission_set_group_serializer.rb +12 -0
  241. data/lib/permission_forge/api/serializers/permission_set_group_user_serializer.rb +11 -0
  242. data/lib/permission_forge/api/serializers/permission_set_rule_serializer.rb +25 -0
  243. data/lib/permission_forge/api/serializers/permission_set_serializer.rb +13 -0
  244. data/lib/permission_forge/api/serializers/permission_set_user_serializer.rb +11 -0
  245. data/lib/permission_forge/api/serializers/resource_action_serializer.rb +13 -0
  246. data/lib/permission_forge/api/serializers/resource_field_serializer.rb +14 -0
  247. data/lib/permission_forge/api/serializers/resource_serializer.rb +15 -0
  248. data/lib/permission_forge/audit/actor.rb +144 -0
  249. data/lib/permission_forge/audit/actor_resolver.rb +103 -0
  250. data/lib/permission_forge/audit/audited_views.rb +208 -0
  251. data/lib/permission_forge/audit/boot_warnings.rb +58 -0
  252. data/lib/permission_forge/audit/column_values.rb +56 -0
  253. data/lib/permission_forge/audit/context.rb +98 -0
  254. data/lib/permission_forge/audit/detached.rb +62 -0
  255. data/lib/permission_forge/audit/event_catalog.rb +110 -0
  256. data/lib/permission_forge/audit/event_query.rb +192 -0
  257. data/lib/permission_forge/audit/failure.rb +47 -0
  258. data/lib/permission_forge/audit/field_access.rb +186 -0
  259. data/lib/permission_forge/audit/gate.rb +167 -0
  260. data/lib/permission_forge/audit/job_context.rb +32 -0
  261. data/lib/permission_forge/audit/log_access.rb +36 -0
  262. data/lib/permission_forge/audit/recorder.rb +168 -0
  263. data/lib/permission_forge/audit/refusals.rb +61 -0
  264. data/lib/permission_forge/audit/request_context.rb +152 -0
  265. data/lib/permission_forge/audit/runtime.rb +178 -0
  266. data/lib/permission_forge/audit/runtime_emission.rb +135 -0
  267. data/lib/permission_forge/audit/settings_observer.rb +70 -0
  268. data/lib/permission_forge/audit/source.rb +54 -0
  269. data/lib/permission_forge/audit/subscribers/base.rb +69 -0
  270. data/lib/permission_forge/audit/subscribers/legacy_hooks.rb +97 -0
  271. data/lib/permission_forge/audit/subscribers/log.rb +45 -0
  272. data/lib/permission_forge/audit/subscribers.rb +58 -0
  273. data/lib/permission_forge/audit.rb +302 -0
  274. data/lib/permission_forge/boot_checks.rb +184 -0
  275. data/lib/permission_forge/cache/backlog.rb +142 -0
  276. data/lib/permission_forge/cache/base.rb +200 -0
  277. data/lib/permission_forge/cache/bulk_invalidation.rb +131 -0
  278. data/lib/permission_forge/cache/failure.rb +171 -0
  279. data/lib/permission_forge/cache/handled_errors.rb +82 -0
  280. data/lib/permission_forge/cache/holder_invalidation.rb +85 -0
  281. data/lib/permission_forge/cache/invalidation.rb +180 -0
  282. data/lib/permission_forge/cache/memory_cache.rb +77 -0
  283. data/lib/permission_forge/cache/null_cache.rb +40 -0
  284. data/lib/permission_forge/cache/rails_cache.rb +231 -0
  285. data/lib/permission_forge/cache/rebuild.rb +149 -0
  286. data/lib/permission_forge/cache/settlement.rb +63 -0
  287. data/lib/permission_forge/cache/user_keys.rb +206 -0
  288. data/lib/permission_forge/cache/variant_removal.rb +55 -0
  289. data/lib/permission_forge/cache/withheld_notice.rb +60 -0
  290. data/lib/permission_forge/concerns/audited_collection.rb +34 -0
  291. data/lib/permission_forge/concerns/auto_permission_scoping.rb +158 -0
  292. data/lib/permission_forge/concerns/controller_action_guard.rb +106 -0
  293. data/lib/permission_forge/concerns/field_mask_checker.rb +45 -0
  294. data/lib/permission_forge/concerns/has_active_scope.rb +12 -0
  295. data/lib/permission_forge/concerns/has_audit_trail.rb +59 -0
  296. data/lib/permission_forge/concerns/has_permission_set_users.rb +115 -0
  297. data/lib/permission_forge/concerns/has_tenant_scoping.rb +86 -0
  298. data/lib/permission_forge/concerns/has_uuid.rb +24 -0
  299. data/lib/permission_forge/concerns/invalidates_in_batch.rb +207 -0
  300. data/lib/permission_forge/concerns/invalidates_managed_actions.rb +80 -0
  301. data/lib/permission_forge/concerns/masked_attributes/alias_access.rb +280 -0
  302. data/lib/permission_forge/concerns/masked_attributes/store_access.rb +100 -0
  303. data/lib/permission_forge/concerns/masked_attributes/validation_reads.rb +153 -0
  304. data/lib/permission_forge/concerns/masked_attributes/virtual_readers.rb +242 -0
  305. data/lib/permission_forge/concerns/masked_attributes.rb +576 -0
  306. data/lib/permission_forge/concerns/permission_cache_accessor.rb +416 -0
  307. data/lib/permission_forge/concerns/permission_scoped_findable.rb +337 -0
  308. data/lib/permission_forge/concerns/permissioned_user.rb +78 -0
  309. data/lib/permission_forge/concerns/permissions_memo.rb +160 -0
  310. data/lib/permission_forge/concerns/permissions_per_context.rb +288 -0
  311. data/lib/permission_forge/concerns/permissions_regenerator.rb +43 -0
  312. data/lib/permission_forge/concerns/records_audit_events.rb +286 -0
  313. data/lib/permission_forge/concerns/resource_permission_checker.rb +116 -0
  314. data/lib/permission_forge/concerns/validates_column_lengths.rb +69 -0
  315. data/lib/permission_forge/concerns/validates_dependency.rb +54 -0
  316. data/lib/permission_forge/conditional_sets.rb +361 -0
  317. data/lib/permission_forge/configuration/admin_ui_settings.rb +108 -0
  318. data/lib/permission_forge/configuration/audit_settings.rb +184 -0
  319. data/lib/permission_forge/configuration/cache_settings.rb +76 -0
  320. data/lib/permission_forge/configuration/controller_action_settings.rb +112 -0
  321. data/lib/permission_forge/configuration/gdpr_settings.rb +70 -0
  322. data/lib/permission_forge/configuration/masked_attributes_settings.rb +83 -0
  323. data/lib/permission_forge/configuration/permits_attributes_settings.rb +26 -0
  324. data/lib/permission_forge/configuration/presets.rb +139 -0
  325. data/lib/permission_forge/configuration/query_scoping_settings.rb +107 -0
  326. data/lib/permission_forge/configuration/strategy_values.rb +57 -0
  327. data/lib/permission_forge/configuration/virtual_attributes_settings.rb +65 -0
  328. data/lib/permission_forge/configuration.rb +624 -0
  329. data/lib/permission_forge/controller_actions/authorizer.rb +171 -0
  330. data/lib/permission_forge/controller_actions/managed_index.rb +213 -0
  331. data/lib/permission_forge/controller_actions/mapping.rb +167 -0
  332. data/lib/permission_forge/controller_actions.rb +122 -0
  333. data/lib/permission_forge/deprecation.rb +86 -0
  334. data/lib/permission_forge/engine.rb +113 -0
  335. data/lib/permission_forge/execution_state.rb +318 -0
  336. data/lib/permission_forge/invalidation_batch.rb +206 -0
  337. data/lib/permission_forge/masked_models.rb +226 -0
  338. data/lib/permission_forge/masking_sync.rb +271 -0
  339. data/lib/permission_forge/model_bindings.rb +90 -0
  340. data/lib/permission_forge/open_transaction.rb +303 -0
  341. data/lib/permission_forge/operators/constants.rb +41 -0
  342. data/lib/permission_forge/operators/registry.rb +300 -0
  343. data/lib/permission_forge/operators/rule_column.rb +120 -0
  344. data/lib/permission_forge/operators/validations.rb +111 -0
  345. data/lib/permission_forge/operators/value_casters/number.rb +68 -0
  346. data/lib/permission_forge/operators/value_casters/temporal.rb +69 -0
  347. data/lib/permission_forge/operators/value_casters.rb +80 -0
  348. data/lib/permission_forge/operators/value_typing.rb +115 -0
  349. data/lib/permission_forge/query_scoping_warnings.rb +68 -0
  350. data/lib/permission_forge/rake_audit.rb +65 -0
  351. data/lib/permission_forge/resource_chain.rb +190 -0
  352. data/lib/permission_forge/rls.rb +209 -0
  353. data/lib/permission_forge/services/attribute_access_guard.rb +138 -0
  354. data/lib/permission_forge/services/audit_event_purger.rb +150 -0
  355. data/lib/permission_forge/services/bulk_permission_set_assignment.rb +108 -0
  356. data/lib/permission_forge/services/concerns/across_tenants.rb +69 -0
  357. data/lib/permission_forge/services/concerns/audited_operation.rb +43 -0
  358. data/lib/permission_forge/services/concerns/clonable.rb +29 -0
  359. data/lib/permission_forge/services/consent_withdrawal_handler.rb +135 -0
  360. data/lib/permission_forge/services/dashboard_stats.rb +43 -0
  361. data/lib/permission_forge/services/data_subject_erasure.rb +306 -0
  362. data/lib/permission_forge/services/expired_assignment_cleaner.rb +152 -0
  363. data/lib/permission_forge/services/extra_field_defaults.rb +71 -0
  364. data/lib/permission_forge/services/field_permission_merger.rb +83 -0
  365. data/lib/permission_forge/services/filters_response_attributes.rb +144 -0
  366. data/lib/permission_forge/services/gdpr_field_auditor/text_report.rb +63 -0
  367. data/lib/permission_forge/services/gdpr_field_auditor.rb +223 -0
  368. data/lib/permission_forge/services/host_key_guard.rb +110 -0
  369. data/lib/permission_forge/services/mask_value.rb +93 -0
  370. data/lib/permission_forge/services/permission_cache_clearer.rb +174 -0
  371. data/lib/permission_forge/services/permission_hash_builder.rb +305 -0
  372. data/lib/permission_forge/services/permission_rule_applier.rb +192 -0
  373. data/lib/permission_forge/services/permission_set_cloner.rb +89 -0
  374. data/lib/permission_forge/services/permission_set_condition_evaluator.rb +102 -0
  375. data/lib/permission_forge/services/permission_set_group_cloner.rb +52 -0
  376. data/lib/permission_forge/services/permits_attributes/nested_records.rb +106 -0
  377. data/lib/permission_forge/services/permits_attributes.rb +270 -0
  378. data/lib/permission_forge/services/resource_field_generator.rb +93 -0
  379. data/lib/permission_forge/services/resource_with_fields_builder.rb +47 -0
  380. data/lib/permission_forge/services/rls_operator_sql.rb +131 -0
  381. data/lib/permission_forge/services/rls_policy_generator.rb +365 -0
  382. data/lib/permission_forge/services/security_audit/action_check.rb +157 -0
  383. data/lib/permission_forge/services/security_audit/assignment_check.rb +120 -0
  384. data/lib/permission_forge/services/security_audit/audit_trail_check.rb +166 -0
  385. data/lib/permission_forge/services/security_audit/base_check.rb +57 -0
  386. data/lib/permission_forge/services/security_audit/config_check.rb +212 -0
  387. data/lib/permission_forge/services/security_audit/integrity_check.rb +105 -0
  388. data/lib/permission_forge/services/security_audit/personal_data_check.rb +79 -0
  389. data/lib/permission_forge/services/security_audit/privilege_check.rb +67 -0
  390. data/lib/permission_forge/services/security_audit/rule_check.rb +141 -0
  391. data/lib/permission_forge/services/security_audit/text_report.rb +46 -0
  392. data/lib/permission_forge/services/security_audit/user_model_check.rb +67 -0
  393. data/lib/permission_forge/services/security_audit.rb +75 -0
  394. data/lib/permission_forge/services/stale_assignment_auditor.rb +68 -0
  395. data/lib/permission_forge/services/unscoped_query_detector.rb +132 -0
  396. data/lib/permission_forge/version.rb +3 -0
  397. data/lib/permission_forge/virtual_fields/discovery.rb +65 -0
  398. data/lib/permission_forge/virtual_fields.rb +102 -0
  399. data/lib/permission_forge.rb +464 -0
  400. data/lib/tasks/permission_forge.rake +31 -0
  401. data/lib/tasks/permission_forge_audit_events.rake +28 -0
  402. data/lib/tasks/permission_forge_cache.rake +26 -0
  403. data/lib/tasks/permission_forge_expiration.rake +30 -0
  404. data/lib/tasks/permission_forge_gdpr.rake +69 -0
  405. data/lib/tasks/permission_forge_rls.rake +38 -0
  406. metadata +515 -0
@@ -0,0 +1,52 @@
1
+ module PermissionForge
2
+ module Services
3
+ # Deep-copies a Permission Set Group with all its Permission Sets (each
4
+ # through PermissionSetCloner) — never its members. The group's copy
5
+ # records `permission_set_group.cloned`, each set's copy
6
+ # `permission_set.cloned`; every event of the copy shares one correlation id.
7
+ #
8
+ # @api public
9
+ class PermissionSetGroupCloner
10
+ include Clonable
11
+
12
+ # @param unlock [Boolean] copy the group's sets (and their rows)
13
+ # editable (`is_editable: true`) instead of keeping their locks
14
+ def self.call(permission_set_group:, attrs: {}, unlock: false)
15
+ new(permission_set_group, attrs, unlock: unlock).call
16
+ end
17
+
18
+ def initialize(permission_set_group, attrs, unlock: false)
19
+ @group = permission_set_group
20
+ @attrs = attrs
21
+ @unlock = unlock
22
+ end
23
+
24
+ # @return [PermissionSetGroup] the copy
25
+ def call
26
+ PermissionForge::Audit.correlate do
27
+ @group.class.transaction do
28
+ cloned = duplicate_record(@group, **@attrs)
29
+ clone_permission_sets(cloned)
30
+ record_clone(cloned)
31
+ cloned
32
+ end
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def clone_permission_sets(cloned_group)
39
+ @group.permission_sets.each do |ps|
40
+ attrs = { name: ps.name, permission_set_group: cloned_group }
41
+ attrs[:is_editable] = true if @unlock
42
+ PermissionSetCloner.call(permission_set: ps, attrs: attrs, unlock: @unlock)
43
+ end
44
+ end
45
+
46
+ def record_clone(cloned)
47
+ PermissionForge::Audit.record('permission_set_group.cloned', record: cloned,
48
+ data: { 'source_uuid' => @group.uuid, 'source_name' => @group.name })
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Services
5
+ class PermitsAttributes
6
+ # Which nested records name a row the current user may reach.
7
+ #
8
+ # A nested record keeps its `id` (without one a payload can only
9
+ # create), and Rails refuses the id of another parent's row — but it
10
+ # loads the rows of the SAME parent through the association, which is
11
+ # not rule-scoped. So a record with an id survives only when the child
12
+ # model has no row-level rules to ask (no PermissionScopedFindable:
13
+ # left to Rails) or the id is inside `permission_scoped` for the current
14
+ # user, checked in one query per payload. Otherwise the whole record
15
+ # goes: dropping the id alone would turn an update into a create. A
16
+ # blank id is a new record, as Rails reads it: nothing to check.
17
+ #
18
+ # @api private
19
+ class NestedRecords
20
+ # One id per record. A plain Hash may carry both 'id' and :id; Rails
21
+ # reads the record with indifferent access (the last key wins), and
22
+ # the guard must not check one id while Rails applies the other. The
23
+ # same id under both keys is kept under one; two ids drop the record.
24
+ #
25
+ # @param record [Hash] a filtered record
26
+ # @return [Hash, nil]
27
+ def self.with_one_id(record)
28
+ return record unless record.key?('id') && record.key?(:id)
29
+
30
+ ids = [record['id'], record[:id]].map { |id| id.to_s.presence }.uniq
31
+ record.except(:id) if ids.size == 1
32
+ end
33
+
34
+ # @param collection [Boolean] whether the association is a collection
35
+ # (has_many); a has_one / belongs_to payload is one record
36
+ def initialize(child, collection:)
37
+ @child = child
38
+ @collection = collection
39
+ end
40
+
41
+ # @param payload [Array<Hash>, Hash] the filtered payload: an Array of
42
+ # records, Rails' index Hash, or one record
43
+ # @yield once for every record of a collection that is dropped
44
+ # @return [Array<Hash>, Hash, nil] the payload without the denied
45
+ # records; nil when its one record is denied
46
+ def admit(payload, &)
47
+ return payload unless @child.respond_to?(:permission_scoped)
48
+ return admit_one(payload) unless @collection
49
+
50
+ case payload
51
+ when Array then admit_each(payload, &)
52
+ when Hash then id_key?(payload) ? admit_one(payload) : admit_indexed(payload, &)
53
+ else payload
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def admit_each(records, &)
60
+ denied = denied_ids(records)
61
+ records.select { |record| admitted?(record, denied, &) }
62
+ end
63
+
64
+ def admit_indexed(records, &)
65
+ denied = denied_ids(records.values)
66
+ records.select { |_, record| admitted?(record, denied, &) }
67
+ end
68
+
69
+ def admit_one(record)
70
+ record if denied_ids([record]).empty?
71
+ end
72
+
73
+ def admitted?(record, denied)
74
+ return true unless denied.include?(id_of(record))
75
+
76
+ yield if block_given?
77
+ false
78
+ end
79
+
80
+ # The ids among the records that permission_scoped does not reach,
81
+ # as Strings, in one query. An id that names no row is denied too.
82
+ def denied_ids(records)
83
+ ids = records.filter_map { |record| id_of(record) }
84
+ return [] if ids.empty?
85
+
86
+ key = @child.primary_key
87
+ ids - @child.permission_scoped.where(key => ids).pluck(key).map(&:to_s)
88
+ end
89
+
90
+ # As Rails tells one record of a collection from the index form: by
91
+ # the key, whatever it holds.
92
+ def id_key?(record)
93
+ record.key?('id') || record.key?(:id)
94
+ end
95
+
96
+ # The record's id as a String; nil for none or a blank one.
97
+ def id_of(record)
98
+ return unless record.respond_to?(:key?)
99
+
100
+ id = record.key?('id') ? record['id'] : record[:id]
101
+ id.to_s.presence
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,270 @@
1
+ module PermissionForge
2
+ module Services
3
+ # Filters permitted params by the current user's field-level EDIT permissions.
4
+ #
5
+ # A key is kept when the user may edit that field on the resource, whatever
6
+ # its value: a Hash or an Array is a value like any other (a json column),
7
+ # never a payload to look into. A key the user may not edit is dropped,
8
+ # whatever its value (fail closed).
9
+ #
10
+ # The one exception is a nested payload: `<assoc>_attributes` for an
11
+ # association the model accepts nested attributes for. It is filtered
12
+ # record by record against the ASSOCIATION MODEL's own resource entry — an
13
+ # Array per element, Rails' index Hash per value, a has_one / belongs_to
14
+ # Hash as one record; an element that is not a record is dropped. A
15
+ # record keeps its `id` only when it names a row the user may reach
16
+ # ({NestedRecords}: inside the child model's `permission_scoped`, or the
17
+ # child has no row rules to ask); a denied record is dropped whole. A
18
+ # record keeps its `_destroy` only when the user may delete the model
19
+ # the payload is filtered for (its entry's `delete_access`; Rails would
20
+ # otherwise destroy the row on the strength of an edit grant). When
21
+ # the user's hash has no entry for that model the payload is dropped;
22
+ # config.nested_attributes_parent_fallback = true filters it against the
23
+ # parent's fields and delete access instead (the id check still applies).
24
+ #
25
+ # Non-mutating: the caller's params object is never modified; a new plain Hash
26
+ # is returned.
27
+ #
28
+ # The keys that were dropped are published as params.filtered (when it is
29
+ # stored or heard; nothing is collected otherwise): their paths —
30
+ # `secret`, `supplier_attributes.secret`, `lines_attributes.*.price`,
31
+ # `lines_attributes.*` for an element that is no record — never their
32
+ # values. Without a user every key is dropped (reason nil_user).
33
+ #
34
+ # @example
35
+ # filtered = PermitsAttributes.call(resource: customer, permitted_params: params)
36
+ #
37
+ # @api public
38
+ class PermitsAttributes
39
+ EVENT = 'params.filtered'.freeze
40
+ NESTED_SUFFIX = '_attributes'.freeze
41
+ # The path segment of a record inside a collection payload.
42
+ EACH = '*'.freeze
43
+ # The keys of a nested record that name it rather than set a field.
44
+ RECORD_KEYS = %w[id].freeze
45
+ # The key of a nested record that asks for its destruction.
46
+ DESTROY_KEY = '_destroy'.freeze
47
+
48
+ # Filters incoming params by the user's field-level edit access (does not mutate them).
49
+ # @param resource [String] the model name
50
+ # @param permitted_params [Hash]
51
+ # @return [Hash]
52
+ def self.call(resource:, permitted_params:)
53
+ new(resource: resource, permitted_params: permitted_params).call
54
+ end
55
+
56
+ def initialize(resource:, permitted_params:)
57
+ @user = PermissionForge.current_user
58
+ @resource = resource
59
+ @permitted_params = permitted_params
60
+ end
61
+
62
+ # @return [Hash] a new hash holding only the fields the user may edit
63
+ def call
64
+ if @user.blank?
65
+ report_dropped('nil_user') { each_pair(@permitted_params) { |key, _| dropped(key) } } if nil_user_report?
66
+ return {}
67
+ end
68
+ return {} if resource_class.nil?
69
+ return {} if @permitted_params.blank?
70
+
71
+ report_dropped('field_access') { filter_for(resource_class, @permitted_params) }
72
+ end
73
+
74
+ private
75
+
76
+ # Runs the filter, collecting the dropped paths in @dropped only when
77
+ # params.filtered is stored or heard, and publishes them.
78
+ def report_dropped(reason)
79
+ return yield unless PermissionForge::Audit.instrument?(EVENT)
80
+
81
+ @dropped = []
82
+ @path = []
83
+ result = yield
84
+ publish_dropped(reason) unless @dropped.empty?
85
+ result
86
+ end
87
+
88
+ def nil_user_report?
89
+ !resource_class.nil? && @permitted_params.present? && PermissionForge::Audit.instrument?(EVENT)
90
+ end
91
+
92
+ def publish_dropped(reason)
93
+ dropped = @dropped.uniq
94
+ record = @resource.is_a?(Class) ? nil : @resource
95
+ PermissionForge::Audit.instrument(EVENT) do
96
+ reference = record ? { record: record } : { record_type: PermissionForge::Audit::Runtime.record_type(resource_class) }
97
+ reference.merge(subject_user_id: PermissionForge::Audit::Runtime.user_id(@user),
98
+ data: { 'model' => resource_class.name, 'reason' => reason, 'dropped' => dropped },
99
+ objects: { user: @user, record: record })
100
+ end
101
+ end
102
+
103
+ # Records the dropped key's path, when dropped keys are collected. Nil,
104
+ # so that a dropped element leaves nothing in a filter_map.
105
+ def dropped(key)
106
+ @dropped&.push((@path + [key.to_s]).join('.'))
107
+ nil
108
+ end
109
+
110
+ def within(segment)
111
+ return yield unless @dropped
112
+
113
+ @path.push(segment.to_s)
114
+ begin
115
+ yield
116
+ ensure
117
+ @path.pop
118
+ end
119
+ end
120
+
121
+ def resource_class
122
+ return nil if @resource.nil?
123
+
124
+ @resource.is_a?(Class) ? @resource : @resource.class
125
+ end
126
+
127
+ # A NEW hash with the keys editable on `klass`, each nested payload
128
+ # filtered for its own model. Everything else is dropped (fail closed).
129
+ def filter_for(klass, params, record: false)
130
+ fields = editable_fields_for(klass)
131
+ result = {}
132
+
133
+ each_pair(params) do |key, value|
134
+ if (association = nested_association(klass, key))
135
+ filtered = within(key) { filter_nested(klass, association, value) }
136
+ filtered.nil? ? dropped(key) : result[key] = filtered
137
+ elsif fields.key?(key.to_sym) || (record && record_key?(klass, key))
138
+ result[key] = value
139
+ else
140
+ dropped(key)
141
+ end
142
+ end
143
+
144
+ result
145
+ end
146
+
147
+ # A key of a nested record that names it rather than sets a field —
148
+ # or its `_destroy`, when the user may delete records of `klass`.
149
+ def record_key?(klass, key)
150
+ name = key.to_s
151
+ RECORD_KEYS.include?(name) || (name == DESTROY_KEY && deletable?(klass))
152
+ end
153
+
154
+ # Whether the user may delete records of `klass`: the delete_access of
155
+ # the entry the class answers to (ResourceChain.grants?). Once per
156
+ # class and call.
157
+ def deletable?(klass)
158
+ @deletable ||= {}
159
+ @deletable.fetch(klass) do
160
+ @deletable[klass] = @user.respond_to?(:permissions_hash) &&
161
+ PermissionForge::ResourceChain.grants?(@user, resource_names(klass), :delete_access)
162
+ end
163
+ end
164
+
165
+ # The association behind a `<assoc>_attributes` key, when `klass`
166
+ # accepts nested attributes for it. Any other key is a field.
167
+ def nested_association(klass, key)
168
+ name = key.to_s
169
+ return unless name.end_with?(NESTED_SUFFIX) && klass.respond_to?(:nested_attributes_options)
170
+
171
+ assoc_name = name.delete_suffix(NESTED_SUFFIX).to_sym
172
+ klass.reflect_on_association(assoc_name) if klass.nested_attributes_options.key?(assoc_name)
173
+ end
174
+
175
+ # The payload filtered for the model it is for, minus the records whose
176
+ # id names a row the child's row rules deny (NestedRecords) — or nil,
177
+ # the key dropped as a whole: no entry for that model, not a payload,
178
+ # or its one record denied.
179
+ def filter_nested(parent_klass, association, value)
180
+ child = association_class(association)
181
+ target = nested_target(parent_klass, child)
182
+ return unless target
183
+
184
+ filtered = filter_payload(target, association, value)
185
+ NestedRecords.new(child, collection: association.collection?).admit(filtered) { dropped(EACH) } unless filtered.nil?
186
+ end
187
+
188
+ # The model a nested payload is filtered for: the association's own,
189
+ # when the user's hash has an entry for it; the parent, only under
190
+ # config.nested_attributes_parent_fallback (a key editable on the
191
+ # parent then admits the same key on a child the user was never
192
+ # granted); nothing otherwise.
193
+ def nested_target(parent_klass, child)
194
+ return child if child && configured_resource?(child)
195
+
196
+ parent_klass if PermissionForge.configuration.nested_attributes_parent_fallback
197
+ end
198
+
199
+ # nil for a polymorphic association (no one model) or one whose model
200
+ # does not load: such a payload is dropped.
201
+ def association_class(association)
202
+ association.klass unless association.polymorphic?
203
+ rescue NameError
204
+ nil
205
+ end
206
+
207
+ def filter_payload(target, association, value)
208
+ return filter_record(target, value) unless association.collection?
209
+ return filter_each(target, value) if value.is_a?(Array)
210
+ return unless each_pair?(value)
211
+
212
+ # As Rails reads a collection Hash: one record when it has an id,
213
+ # otherwise the index form, whose values are the records.
214
+ record_with_id?(value) ? within(EACH) { filter_record(target, value) } : filter_indexed(target, value)
215
+ end
216
+
217
+ # One record (a has_one / belongs_to payload, or one of a collection),
218
+ # with one id (NestedRecords.with_one_id); nil when it is no record or
219
+ # names two ids.
220
+ def filter_record(target, value)
221
+ NestedRecords.with_one_id(filter_for(target, value, record: true)) if each_pair?(value)
222
+ end
223
+
224
+ def filter_each(target, records)
225
+ records.filter_map do |record|
226
+ filtered = within(EACH) { filter_record(target, record) }
227
+ filtered.nil? ? dropped(EACH) : filtered
228
+ end
229
+ end
230
+
231
+ def filter_indexed(target, records)
232
+ result = {}
233
+ each_pair(records) do |index, record|
234
+ filtered = within(EACH) { filter_record(target, record) }
235
+ filtered.nil? ? dropped(EACH) : result[index] = filtered
236
+ end
237
+ result
238
+ end
239
+
240
+ def record_with_id?(value)
241
+ value.key?('id') || value.key?(:id)
242
+ end
243
+
244
+ # The Resource names a model answers to: its own and its STI
245
+ # ancestors' (ResourceChain).
246
+ def resource_names(klass)
247
+ PermissionForge::ResourceChain.names(klass)
248
+ end
249
+
250
+ def configured_resource?(klass)
251
+ @user.respond_to?(:permissions_hash) && PermissionForge::ResourceChain.entries(@user, resource_names(klass)).any?
252
+ end
253
+
254
+ def editable_fields_for(klass)
255
+ return {} unless @user.respond_to?(:permissions_hash)
256
+
257
+ PermissionForge::ResourceChain.fields(@user, resource_names(klass)).select { |_, access| access[:edit_access] }
258
+ end
259
+
260
+ # Iterates key/value pairs of an ActionController::Parameters or a Hash.
261
+ def each_pair(params, &)
262
+ params.each_pair(&)
263
+ end
264
+
265
+ def each_pair?(value)
266
+ value.respond_to?(:each_pair)
267
+ end
268
+ end
269
+ end
270
+ end
@@ -0,0 +1,93 @@
1
+ module PermissionForge
2
+ module Services
3
+ # Creates a ResourceField for every column of a resource's model (and declared virtual fields
4
+ # under `extra_fields_default_access = :unconfigured`).
5
+ #
6
+ # @api public
7
+ class ResourceFieldGenerator
8
+ # @param resource [Resource]
9
+ # @param grant_all [Boolean] grant read and edit on the generated fields
10
+ # @return [Array<ResourceField>] the fields created
11
+ def self.call(resource:, grant_all: false)
12
+ new(resource: resource, grant_all: grant_all).call
13
+ end
14
+
15
+ def initialize(resource:, grant_all: false)
16
+ @resource = resource
17
+ @grant_all = grant_all
18
+ end
19
+
20
+ # All or nothing, under one audit correlation id (an enclosing block's,
21
+ # if any): a field that cannot be saved leaves no field and no event.
22
+ def call
23
+ model_class = @resource.name.safe_constantize
24
+ return [] unless model_class
25
+
26
+ fields = PermissionForge::Audit.correlate { PermissionForge::ResourceField.transaction { generate_fields(model_class) } }
27
+ PermissionForge.configuration.record_gdpr_pattern_checksum
28
+ fields
29
+ end
30
+
31
+ # A field needs a name: a blank one declared by the model is skipped
32
+ # (and logged) rather than failing "Add resource" / "Sync fields".
33
+ def self.without_blank_names(model_class, names)
34
+ usable = names.reject { |name| name.to_s.strip.empty? }
35
+ skipped = names.size - usable.size
36
+ if skipped.positive?
37
+ Rails.logger&.warn("[PermissionForge] skipped #{skipped} blank field name(s) declared by " \
38
+ "#{model_class.name}.#{PermissionForge.configuration.extra_fields_method}")
39
+ end
40
+ usable
41
+ end
42
+
43
+ private
44
+
45
+ def generate_fields(model_class)
46
+ existing_names = @resource.resource_fields.pluck(:name)
47
+ columns = model_class.column_names - ignored_fields + virtual_field_names(model_class) - existing_names
48
+
49
+ columns.map do |column_name|
50
+ attrs = build_field_attrs(column_name)
51
+ apply_gdpr_defaults(attrs, column_name) if PermissionForge.configuration.gdpr_defaults
52
+ PermissionForge::ResourceField.create!(attrs)
53
+ end
54
+ end
55
+
56
+ def build_field_attrs(column_name)
57
+ {
58
+ resource: @resource,
59
+ name: column_name,
60
+ label: column_name.humanize,
61
+ is_active: true,
62
+ is_editable: true,
63
+ read_access: @grant_all,
64
+ edit_access: @grant_all
65
+ }
66
+ end
67
+
68
+ def ignored_fields
69
+ PermissionForge.configuration.ignored_fields
70
+ end
71
+
72
+ # Virtual fields get rows only under extra_fields_default_access =
73
+ # :unconfigured. Under :grant ExtraFieldDefaults already gives declared
74
+ # names read + edit, and a generated row (no access unless grant_all)
75
+ # would take that away from callers that rely on it.
76
+ def virtual_field_names(model_class)
77
+ configuration = PermissionForge.configuration
78
+ return [] if configuration.grant_extra_fields_by_default?
79
+
80
+ self.class.without_blank_names(model_class, PermissionForge::VirtualFields.new(model_class, configuration).names)
81
+ end
82
+
83
+ def apply_gdpr_defaults(attrs, column_name)
84
+ _pattern, strategy = PermissionForge.configuration.gdpr_field_patterns.find { |regex, _| column_name.match?(regex) }
85
+ return unless strategy
86
+
87
+ attrs[:read_access] = true
88
+ attrs[:mask_access] = true
89
+ attrs[:mask_strategy] = strategy
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,47 @@
1
+ module PermissionForge
2
+ module Services
3
+ # Creates a Resource with full CRUD access and its generated fields, all or nothing.
4
+ #
5
+ # @api public
6
+ class ResourceWithFieldsBuilder
7
+ # @param permission_set [PermissionSet]
8
+ # @param resource_name [String] the model name
9
+ # @return [Resource]
10
+ def self.call(permission_set:, resource_name:)
11
+ new(permission_set, resource_name).call
12
+ end
13
+
14
+ def initialize(permission_set, resource_name)
15
+ @permission_set = permission_set
16
+ @resource_name = resource_name
17
+ end
18
+
19
+ # The resource and its fields are created all or nothing, under one
20
+ # audit correlation id (an enclosing block's, if any).
21
+ def call
22
+ PermissionForge::Audit.correlate do
23
+ PermissionForge::Resource.transaction do
24
+ resource = create_resource
25
+ ResourceFieldGenerator.call(resource: resource, grant_all: true)
26
+ resource
27
+ end
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def create_resource
34
+ PermissionForge::Resource.create!(
35
+ name: @resource_name,
36
+ permission_set: @permission_set,
37
+ read_access: true,
38
+ create_access: true,
39
+ edit_access: true,
40
+ delete_access: true,
41
+ is_active: true,
42
+ is_editable: true
43
+ )
44
+ end
45
+ end
46
+ end
47
+ end