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,280 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Concerns
5
+ module MaskedAttributes
6
+ # `alias_attribute` on a model with MaskedAttributes. An alias is not a
7
+ # field of its own: reading or writing it is reading or writing the
8
+ # attribute it points to, at that attribute's level.
9
+ #
10
+ # Rails generates an alias's reader and writer to reach the attribute
11
+ # directly — always on Rails 7.2 and later, and on 7.1 unless the model
12
+ # defines the target's method itself — past the masked getter and the
13
+ # guarded setter. So every alias gets a reader and a writer here that do
14
+ # what the Rails version in use does, plus the guard (see
15
+ # {AttributeAccessors} and {TargetMethodAccessors}).
16
+ #
17
+ # Where they live decides which of the model's own methods still run.
18
+ # Rails keeps an alias's methods in the generated-attribute-methods
19
+ # module of the class that declares it; the accessors go in a module
20
+ # prepended to that very module. Whatever is found before Rails' own
21
+ # alias methods — the class's methods, modules it includes, subclasses —
22
+ # is found before these too, and `super` there reaches them.
23
+ #
24
+ # They never call `super` themselves: Ruby (3.4 at least) does not
25
+ # notice that the method behind a module prepended to an already
26
+ # included module was redefined, and would keep calling the old one.
27
+ #
28
+ # The other methods Rails generates for an alias (`mail?`, `mail_was`,
29
+ # `mail_changed?`, `mail_before_type_cast`…) are already the target's
30
+ # (`email?`, `email_was`…) under another name and are left alone.
31
+ #
32
+ # @api private
33
+ module AliasAccess
34
+ extend ActiveSupport::Concern
35
+
36
+ # The module a class's alias readers and writers live in.
37
+ class AccessorModule < Module; end
38
+
39
+ # `id_value`, which Rails itself aliases to `id`, stays Rails': `id`
40
+ # is the primary key reader, not the reader of a column called id.
41
+ UNMASKED_TARGET = 'id'
42
+
43
+ CREATION = Mutex.new
44
+ private_constant :CREATION
45
+
46
+ # Rails 7.2 and later: an alias reads and writes the attribute itself,
47
+ # whatever reader or setter the model defines for the target. So do
48
+ # these: through `read_attribute` when the target's reader is one the
49
+ # concern masks, and past the write-back guard when the target is a
50
+ # masked column.
51
+ #
52
+ # An alias declared by a class without the concern (a parent of the
53
+ # one that has it) is reached by records of its other subclasses too.
54
+ # For those, this is what Rails generates, word for word. Such a class
55
+ # may declare the alias again with another target without the concern
56
+ # hearing of it, so the target is asked of the declaring class at each
57
+ # call, as Rails generates the alias's methods again there.
58
+ module AttributeAccessors
59
+ def self.define(klass, accessors, name, declared)
60
+ accessors.redefine_method(name, &reader(klass, name, declared))
61
+ accessors.redefine_method(:"#{name}=", &writer(klass, name, declared))
62
+ end
63
+
64
+ def self.reader(klass, name, declared)
65
+ proc do
66
+ target = klass.attribute_aliases.fetch(name, declared)
67
+ if is_a?(MaskedAttributes) && pf_masked_reader?(target)
68
+ read_attribute(target) { |missing| missing_attribute(missing, caller) }
69
+ else
70
+ _read_attribute(target) { |missing| missing_attribute(missing, caller) }
71
+ end
72
+ end
73
+ end
74
+
75
+ def self.writer(klass, name, declared)
76
+ proc do |value|
77
+ target = klass.attribute_aliases.fetch(name, declared)
78
+ next if is_a?(MaskedAttributes) && pf_masked_column?(target) && pf_skip_masked_write?(target, value)
79
+
80
+ _write_attribute(target, value)
81
+ end
82
+ end
83
+ end
84
+
85
+ # Rails 7.1: an alias calls the target's own reader or setter when the
86
+ # model defines it (deprecated there) or the target is not an
87
+ # attribute, and otherwise reads and writes the attribute as later
88
+ # versions do. So do these.
89
+ module TargetMethodAccessors
90
+ def self.define(klass, accessors, name, declared)
91
+ own = OwnMethods.new(klass, name, declared)
92
+ attribute_reader = AttributeAccessors.reader(klass, name, declared)
93
+ attribute_writer = AttributeAccessors.writer(klass, name, declared)
94
+ accessors.redefine_method(name) do
95
+ (reader = own.reader) ? send(reader) : instance_exec(&attribute_reader)
96
+ end
97
+ accessors.redefine_method(:"#{name}=") do |value|
98
+ (setter = own.setter) ? send(setter, value) : instance_exec(value, &attribute_writer)
99
+ end
100
+ end
101
+
102
+ # The target's methods an alias calls on Rails 7.1. The choice is
103
+ # Rails' own (`alias_attribute_method_definition`), made once per
104
+ # method on the class that declares the alias, with the concern's
105
+ # modules looked past: seen by Rails, the masked getter is a method
106
+ # the model defines.
107
+ class OwnMethods
108
+ def initialize(klass, name, declared)
109
+ @klass = klass
110
+ @name = name
111
+ @declared = declared
112
+ @choices = {}
113
+ end
114
+
115
+ # @return [String, nil] the reader to call; nil to read the attribute
116
+ def reader
117
+ own(target)
118
+ end
119
+
120
+ # @return [String, nil] the setter to call; nil to write the attribute
121
+ def setter
122
+ own("#{target}=")
123
+ end
124
+
125
+ private
126
+
127
+ def target
128
+ @klass.attribute_aliases.fetch(@name, @declared)
129
+ end
130
+
131
+ def own(method_name)
132
+ @choices.fetch(method_name) { @choices[method_name] = (method_name if defined_by_the_model?(method_name)) }
133
+ end
134
+
135
+ def defined_by_the_model?(method_name)
136
+ return true unless @klass.abstract_class? || @klass.has_attribute?(target)
137
+ return false if ActiveRecord::AttributeMethods.dangerous_attribute_methods.include?(method_name)
138
+
139
+ owner = owner_outside_the_concern(method_name)
140
+ !owner.nil? && !owner.is_a?(ActiveRecord::AttributeMethods::GeneratedAttributeMethods)
141
+ end
142
+
143
+ def owner_outside_the_concern(method_name)
144
+ method = @klass.instance_method(method_name)
145
+ method = method.super_method while method && concern_module?(method.owner)
146
+ method&.owner
147
+ rescue NameError
148
+ nil
149
+ end
150
+
151
+ def concern_module?(owner)
152
+ [ColumnAccessors, VirtualReaders::ReaderModule, AccessorModule].any? { |kind| owner.is_a?(kind) }
153
+ end
154
+ end
155
+ end
156
+
157
+ class << self
158
+ # Defines the reader and the writer of one alias `klass` declares.
159
+ def define_accessors(klass, name, target)
160
+ target = target.to_s
161
+ return if target == UNMASKED_TARGET
162
+
163
+ accessors.define(klass, accessor_module(klass), name.to_s, target)
164
+ end
165
+
166
+ # Covers the aliases `klass` itself declared, where Rails generates
167
+ # their methods: in `klass`, not in the classes that inherit them.
168
+ def mask_aliases_declared_by(klass)
169
+ declared_aliases(klass).each { |name, target| define_accessors(klass, name, target) }
170
+ end
171
+
172
+ # @return [String, nil] the attribute `name` is an alias of on
173
+ # `klass`, following aliases of aliases
174
+ def target(klass, name)
175
+ aliases = klass.attribute_aliases
176
+ target = aliases[name.to_s]
177
+ target && chain_end(aliases, target)
178
+ end
179
+
180
+ # The attribute at the end of a chain of aliases that starts with
181
+ # `target`. Bounded, so aliases that point at each other end too.
182
+ def chain_end(aliases, target)
183
+ aliases.size.times { target = aliases.fetch(target) { return target } }
184
+ target
185
+ end
186
+
187
+ private
188
+
189
+ def accessors
190
+ @accessors ||= ActiveRecord.version < Gem::Version.new('7.2') ? TargetMethodAccessors : AttributeAccessors
191
+ end
192
+
193
+ # The class's own module, prepended to the module Rails generates
194
+ # the class's attribute methods into.
195
+ def accessor_module(klass)
196
+ CREATION.synchronize do
197
+ next klass.const_get(:MaskedAliasAccessors, false) if klass.const_defined?(:MaskedAliasAccessors, false)
198
+
199
+ klass.const_set(:MaskedAliasAccessors, AccessorModule.new)
200
+ .tap { |accessors| klass.send(:generated_attribute_methods).prepend(accessors) }
201
+ end
202
+ end
203
+
204
+ # Rails keeps the aliases each class declares in a registry of its
205
+ # own; without one, the aliases that differ from the superclass's.
206
+ def declared_aliases(klass)
207
+ return registered_aliases(klass) if klass.respond_to?(:aliases_by_attribute_name)
208
+
209
+ inherited = klass.superclass.attribute_aliases
210
+ klass.attribute_aliases.reject { |name, target| inherited[name] == target }.to_a
211
+ end
212
+
213
+ # The registry keeps the old target of an alias that was declared
214
+ # again with another one; only what the alias points to now counts.
215
+ def registered_aliases(klass)
216
+ current = klass.attribute_aliases
217
+ klass.aliases_by_attribute_name
218
+ .flat_map { |target, names| names.map { |name| [name, target] } }
219
+ .select { |name, target| current[name] == target }
220
+ end
221
+ end
222
+
223
+ # Extended into the model by ActiveSupport::Concern.
224
+ module ClassMethods
225
+ # The accessors are defined before Rails registers the alias, so
226
+ # they are in place however soon Rails generates its own.
227
+ def alias_attribute(new_name, old_name)
228
+ AliasAccess.define_accessors(self, new_name, old_name)
229
+ super
230
+ end
231
+
232
+ private
233
+
234
+ # For the aliases that were declared before the concern was
235
+ # included, each in the class that declared it: this class, its
236
+ # ancestors (which do not have the concern) and the subclasses that
237
+ # are loaded.
238
+ def pf_mask_attribute_aliases
239
+ lineage = ancestors.grep(Class).take_while { |klass| klass < ActiveRecord::Base }
240
+ (lineage + descendants).each { |klass| AliasAccess.mask_aliases_declared_by(klass) }
241
+ end
242
+ end
243
+
244
+ # `methods:` names that are aliases of an attribute the current user
245
+ # may not read are dropped, the way the attribute itself is dropped
246
+ # from the attribute names. Only a loaded attribute is: reading an
247
+ # alias of one that was not loaded raises, as on any model.
248
+ #
249
+ # This module is included before MaskedAttributes itself (it is a
250
+ # dependency): while the concern is being included, on another
251
+ # thread, a record answers as it did without it.
252
+ def serializable_hash(options = nil)
253
+ return super unless is_a?(MaskedAttributes)
254
+
255
+ super(pf_without_hidden_alias_methods(options))
256
+ end
257
+
258
+ private
259
+
260
+ # The attribute an alias that points to `target` is judged as.
261
+ def pf_alias_end(target)
262
+ AliasAccess.chain_end(self.class.attribute_aliases, target)
263
+ end
264
+
265
+ def pf_without_hidden_alias_methods(options)
266
+ methods = options && options[:methods]
267
+ return options unless methods && (pf_guarded_user || pf_hide_without_user?) &&
268
+ PermissionForge.configuration.omit_hidden_in_attributes_hash
269
+
270
+ options.merge(methods: Array(methods).reject { |name| pf_hidden_alias?(name) })
271
+ end
272
+
273
+ def pf_hidden_alias?(name)
274
+ target = AliasAccess.target(self.class, name)
275
+ target && has_attribute?(target) && pf_hidden_attribute?(target)
276
+ end
277
+ end
278
+ end
279
+ end
280
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Concerns
5
+ module MaskedAttributes
6
+ # Store accessors (`store :settings, accessors: [:theme]`,
7
+ # `store_accessor`) reach the store column through its public getter —
8
+ # the masked one. Left alone, a hidden or masked store column makes Rails
9
+ # see nil (or the mask) where it expects the stored hash: depending on
10
+ # the Rails version the read raises, or Rails replaces the column with an
11
+ # empty hash in memory and a later save wipes the stored data.
12
+ #
13
+ # - Reading a key of a store column the user may not read in full
14
+ # returns the hidden value and never touches the column.
15
+ # - Writing a key works on the real stored hash, so the other keys
16
+ # survive. The accessors and the column getter still return nothing
17
+ # readable afterwards.
18
+ #
19
+ # Only stores whose getter this concern masks are affected (a column, or
20
+ # a wrapped virtual reader while virtual masking is on); any other store
21
+ # is left to Rails.
22
+ #
23
+ # @api private
24
+ module StoreAccess
25
+ # Fiber-local ExecutionState slot: map (by identity) of record => the
26
+ # store column an accessor is writing. Kept beside the record, like
27
+ # the virtual-masking escape hatch, so a copy made during the write
28
+ # or another thread holding the record never reads the column raw.
29
+ RAW_STORE_KEY = :pf_raw_store_columns
30
+
31
+ private
32
+
33
+ def read_store_attribute(store_attribute, key)
34
+ return super if pf_store_column_readable?(store_attribute) || pf_store_column_unloaded?(store_attribute)
35
+
36
+ PermissionForge.configuration.hidden_attribute_value
37
+ end
38
+
39
+ def write_store_attribute(store_attribute, key, value)
40
+ pf_with_raw_store_column(store_attribute) { super }
41
+ end
42
+
43
+ # The attribute Rails reads the store through: a store may be declared
44
+ # on an alias_attribute name.
45
+ def pf_store_column_name(store_attribute)
46
+ name = store_attribute.to_s
47
+ self.class.attribute_aliases[name] || name
48
+ end
49
+
50
+ # Readable in full for the current user. Asked of the guard with a
51
+ # probe rather than by comparing stored values: a store that is nil
52
+ # (a NULL json column, a new record) must not look "readable".
53
+ def pf_store_column_readable?(store_attribute)
54
+ name = pf_store_column_name(store_attribute)
55
+ return true unless pf_masked_reader?(name)
56
+
57
+ include_field, value = pf_guard(name, VirtualReaders::PROBE)
58
+ include_field && value.equal?(VirtualReaders::PROBE)
59
+ end
60
+
61
+ # A store column the record was loaded without (`select(:id)`). Rails
62
+ # raises ActiveModel::MissingAttributeError when an accessor reads or
63
+ # writes one of its keys; hidden or not, it is left to do so.
64
+ def pf_store_column_unloaded?(store_attribute)
65
+ name = pf_store_column_name(store_attribute)
66
+ self.class.has_attribute?(name) && !has_attribute?(name)
67
+ end
68
+
69
+ # Whether the reader of `name` is one this concern masks: the one Rails
70
+ # is about to call for a store, or the target of an alias (AliasAccess).
71
+ def pf_masked_reader?(name)
72
+ pf_masked_column?(name) || (pf_mask_virtual_reads? && self.class.pf_virtual_reader_masked?(name))
73
+ end
74
+
75
+ # A column whose getter is masked and whose setter is guarded.
76
+ def pf_masked_column?(name)
77
+ klass = self.class
78
+ klass.const_defined?(:MaskedAttributeReaders) && klass::MaskedAttributeReaders.method_defined?(name)
79
+ end
80
+
81
+ # Inside the block this record's getter for the store column returns
82
+ # the stored value (see MaskedAttributes#read_attribute). Only that
83
+ # column, only this record on this fiber, and restored when
84
+ # the block ends.
85
+ def pf_with_raw_store_column(store_attribute)
86
+ windows = PermissionForge::ExecutionState.fetch(RAW_STORE_KEY) { {}.compare_by_identity }
87
+ previous = windows[self]
88
+ windows[self] = pf_store_column_name(store_attribute)
89
+ yield
90
+ ensure
91
+ previous ? windows[self] = previous : windows.delete(self)
92
+ end
93
+
94
+ def pf_raw_store_column?(name)
95
+ PermissionForge::ExecutionState[RAW_STORE_KEY]&.[](self) == name
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Concerns
5
+ module MaskedAttributes
6
+ # Validations judge the stored value; what they report shows what the
7
+ # current user reads.
8
+ #
9
+ # A hidden or masked value would fail a presence or format check the
10
+ # stored value passes (nobody signed in under :hide, a user who reads
11
+ # `s***@acme.com` or nothing) and pass a uniqueness check against the
12
+ # mask. So while the validators run (run_validations!), the attribute
13
+ # Rails reads for a validation (read_attribute_for_validation) — that
14
+ # one attribute, nothing else of the record — reads as stored: a derived
15
+ # reader that reads other columns sees them masked, as always.
16
+ #
17
+ # Rails puts the value it validated into the error (value:, which
18
+ # errors.details returns and %{value} interpolates). When the
19
+ # validators are done, every error's value is replaced with what the
20
+ # current user's getter returns, or removed when the user may not read
21
+ # the attribute. A message rendered later reads the getter
22
+ # (read_attribute_for_validation outside the validators), so %{value}
23
+ # in a custom or i18n message shows the masked value or nothing.
24
+ #
25
+ # A Rails EachValidator reads the attribute through
26
+ # read_attribute_for_validation and then, in numericality, again with
27
+ # read_attribute (prepare_value_for_validation). So while an
28
+ # EachValidator judges this record (EachValidatorWindow), the attribute
29
+ # it last read for a validation stays readable as stored until the
30
+ # validator is done; anything else — another validator, a custom
31
+ # `validate` block — reads masked.
32
+ #
33
+ # @api private
34
+ module ValidationReads
35
+ # ExecutionState slots (fiber, by record identity): the records whose
36
+ # validators are running, and the attribute each is reading for one.
37
+ VALIDATING_KEY = :pf_validating_records
38
+ READS_KEY = :pf_validation_reads
39
+ # The records an EachValidator is judging right now.
40
+ WINDOWS_KEY = :pf_validator_windows
41
+
42
+ # Prepended to ActiveModel::EachValidator: keeps the attribute it
43
+ # read for a validation readable as stored until it is done.
44
+ module EachValidatorWindow
45
+ def validate(record)
46
+ return super unless record.is_a?(MaskedAttributes)
47
+
48
+ ValidationReads.within_validator(record) { super }
49
+ end
50
+ end
51
+
52
+ # @api private
53
+ def self.within_validator(record)
54
+ windows = PermissionForge::ExecutionState.fetch(WINDOWS_KEY) { Set.new.compare_by_identity }
55
+ added = windows.add?(record)
56
+ yield
57
+ ensure
58
+ if added
59
+ windows.delete(record)
60
+ PermissionForge::ExecutionState[READS_KEY]&.delete(record)
61
+ end
62
+ end
63
+
64
+ def read_attribute_for_validation(name)
65
+ window = pf_validator_window?
66
+ return super unless pf_validation_read_open?(window)
67
+
68
+ column = pf_validation_column(name)
69
+ pf_reading_for_validation(column || name.to_s, keep: window) do
70
+ column || !VirtualReaders.masked?(self.class, name) ? super : pf_without_virtual_masking { super }
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ def run_validations!
77
+ validating = PermissionForge::ExecutionState.fetch(VALIDATING_KEY) { Set.new.compare_by_identity }
78
+ added = validating.add?(self)
79
+ super
80
+ ensure
81
+ validating&.delete(self) if added
82
+ pf_mask_error_values
83
+ end
84
+
85
+ def pf_validating?
86
+ PermissionForge::ExecutionState[VALIDATING_KEY]&.include?(self) || false
87
+ end
88
+
89
+ # Validating, and no other validation read in progress — or inside an
90
+ # EachValidator, whose next read replaces the last.
91
+ def pf_validation_read_open?(window)
92
+ pf_validating? && (window || !pf_validation_read)
93
+ end
94
+
95
+ # Marks `attribute` as read as stored for the block; inside an
96
+ # EachValidator (keep) it stays marked until the validator is done.
97
+ def pf_reading_for_validation(attribute, keep:)
98
+ reads = PermissionForge::ExecutionState.fetch(READS_KEY) { {}.compare_by_identity }
99
+ reads[self] = attribute
100
+ yield
101
+ ensure
102
+ reads&.delete(self) unless keep
103
+ end
104
+
105
+ def pf_validator_window?
106
+ PermissionForge::ExecutionState[WINDOWS_KEY]&.include?(self) || false
107
+ end
108
+
109
+ # The attribute a validator is reading on this record, or nil.
110
+ def pf_validation_read
111
+ PermissionForge::ExecutionState[READS_KEY]&.[](self)
112
+ end
113
+
114
+ # The column an attribute name reads (an alias as the attribute at
115
+ # the end of its chain), or nil for a method.
116
+ def pf_validation_column(name)
117
+ name = name.to_s
118
+ target = self.class.attribute_aliases[name]
119
+ name = pf_alias_end(target) if target
120
+ name if self.class.attribute_names.include?(name)
121
+ end
122
+
123
+ def pf_mask_error_values
124
+ errors.objects.each do |error|
125
+ options = error.options
126
+ next unless options.key?(:value) && error.attribute != :base
127
+
128
+ include_field, value = pf_error_value(error.attribute)
129
+ include_field ? options[:value] = value : options.delete(:value)
130
+ end
131
+ end
132
+
133
+ # [include?, value]: what the current user reads for the attribute,
134
+ # as a debugging read (no field.access decision). The primary key is
135
+ # never masked. Anything that fails leaves no value.
136
+ def pf_error_value(attribute)
137
+ column = pf_validation_column(attribute)
138
+ pf_while_inspecting do
139
+ if column
140
+ column == self.class.primary_key ? [true, _read_attribute(column)] : pf_guard(column, _read_attribute(column))
141
+ else
142
+ respond_to?(attribute) ? [true, public_send(attribute)] : [false, nil]
143
+ end
144
+ end
145
+ rescue StandardError
146
+ [false, nil]
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
152
+
153
+ ActiveModel::EachValidator.prepend(PermissionForge::Concerns::MaskedAttributes::ValidationReads::EachValidatorWindow)