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,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Services
5
+ module Concerns
6
+ # Runs a housekeeping service under an audit context: who ran it (`actor:`),
7
+ # why (`reason:`), from where, and one correlation id for the per-row events
8
+ # and the summary event it writes.
9
+ #
10
+ # `actor:` is a host user, an Audit::Actor, or a label String (stored as a
11
+ # 'system' actor). nil names nobody: the context already in force (a request's
12
+ # admin, a rake task's ACTOR, PermissionForge.current_user) stays — an explicit
13
+ # nil never replaces a real actor.
14
+ #
15
+ # The source is 'service' unless the caller's context, or the process (rake,
16
+ # console), already says where the call came from.
17
+ module AuditedOperation
18
+ private
19
+
20
+ def audited(actor:, reason:, &)
21
+ PermissionForge::Audit::Context.with(actor: audit_actor_for(actor), source: audit_source, reason: reason) do
22
+ PermissionForge::Audit.correlate(&)
23
+ end
24
+ end
25
+
26
+ # @return [PermissionForge::Audit::Actor, nil]
27
+ def audit_actor_for(actor)
28
+ case actor
29
+ when nil then nil
30
+ when String then PermissionForge::Audit::Actor.system(actor)
31
+ else PermissionForge::Audit::Actor.coerce(actor)
32
+ end
33
+ end
34
+
35
+ def audit_source
36
+ return if PermissionForge::Audit::Context[:source]
37
+
38
+ 'service' if PermissionForge::Audit::Source.detect == 'service'
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,29 @@
1
+ module PermissionForge
2
+ module Services
3
+ # Copies one record as a new row. The copy is a new record in every sense:
4
+ # its own uuid, and no author or editor carried over from the original —
5
+ # HasAuditTrail credits whoever makes the copy, and the copy's create
6
+ # event (RecordsAuditEvents) names the same person. A field's "This is
7
+ # intended" acknowledgement is one person's attestation about one set, so
8
+ # a copy starts unacknowledged.
9
+ #
10
+ # @api private
11
+ module Clonable
12
+ # Columns a copy never takes from its original (those the table has).
13
+ RESET_ON_COPY = %w[uuid created_by_id updated_by_id sensitive_acknowledged_at sensitive_acknowledged_by_id].freeze
14
+
15
+ private
16
+
17
+ def duplicate_record(record, **attrs)
18
+ cloned = record.dup
19
+ cloned.assign_attributes(reset_on_copy(cloned).merge(attrs))
20
+ cloned.save!
21
+ cloned
22
+ end
23
+
24
+ def reset_on_copy(record)
25
+ RESET_ON_COPY.select { |column| record.has_attribute?(column) }.index_with(nil)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,135 @@
1
+ module PermissionForge
2
+ module Services
3
+ # Handles consent withdrawal by removing a user from all permission sets
4
+ # gated on the specified consent flag, then invalidating the user's cached
5
+ # permissions (rebuilt read-through on next access).
6
+ #
7
+ # @example Withdraw marketing consent
8
+ # result = ConsentWithdrawalHandler.call(
9
+ # user: user,
10
+ # consent_flag: 'marketing_consent'
11
+ # )
12
+ # # => { revoked_sets: 2, invalidated: true }
13
+ #
14
+ # `invalidated` is false when nothing was revoked, and when the cache
15
+ # store did not confirm the invalidation (reported through
16
+ # `config.on_cache_failure`).
17
+ #
18
+ # @example Dry run (inspect without revoking)
19
+ # result = ConsentWithdrawalHandler.call(
20
+ # user: user,
21
+ # consent_flag: 'marketing_consent',
22
+ # dry_run: true
23
+ # )
24
+ # # => { revoked_sets: 0, affected_sets: ["Marketing Access"], dry_run: true }
25
+ #
26
+ # The audit log: assignments are destroyed through their callbacks (one
27
+ # `assignment.revoked` each), then ONE `consent.withdrawn` event names the
28
+ # person, the flag, the sets and the counts. `actor:` (a user, an
29
+ # Audit::Actor or a label String) and `reason:` say who ran it and why.
30
+ # A dry run writes nothing.
31
+ #
32
+ # The revocations and `consent.withdrawn` happen in ONE transaction, like
33
+ # DataSubjectErasure: if the summary cannot be written under
34
+ # audit_change_failure_strategy :raise, no assignment is removed. The
35
+ # cache invalidation runs after the commit (so the summary does not say
36
+ # whether the store confirmed it; the result's `invalidated` does).
37
+ #
38
+ # Tenants: with a tenant ambient, the person's assignments in that
39
+ # tenant. With none (a platform task) in every tenant — also under
40
+ # acts_as_tenant's require_tenant — and `consent.withdrawn` is written in
41
+ # each tenant that lost assignments (its sets and count) plus once
42
+ # without a tenant (the totals), each with `across_tenants: true`
43
+ # (Concerns::AcrossTenants).
44
+ #
45
+ # @api public
46
+ class ConsentWithdrawalHandler
47
+ include Concerns::AuditedOperation
48
+ include Concerns::AcrossTenants
49
+
50
+ # Removes a user from every Permission Set gated on the consent flag.
51
+ # @return [Hash] see {#call}
52
+ def self.call(...) = new(...).call
53
+
54
+ def initialize(user:, consent_flag:, dry_run: false, actor: nil, reason: nil)
55
+ @user = user
56
+ @consent_flag = consent_flag
57
+ @dry_run = dry_run
58
+ @actor = actor
59
+ @reason = reason
60
+ end
61
+
62
+ # @return [Hash] `{ revoked_sets:, invalidated: }` (plus `affected_sets:` and `dry_run:` for a dry run)
63
+ def call
64
+ validate!
65
+
66
+ across_tenants do
67
+ affected_sets = consent_gated_sets
68
+ affected_assignments = user_assignments_for(affected_sets)
69
+ next dry_run_result(affected_sets, affected_assignments) if @dry_run
70
+
71
+ audited(actor: @actor, reason: @reason) { withdraw(affected_assignments) }
72
+ end
73
+ end
74
+
75
+ private
76
+
77
+ def withdraw(affected_assignments)
78
+ revoked_count = ActiveRecord::Base.transaction { revoke_and_record(affected_assignments) }
79
+ # what the store confirmed — not what a host's override of the method returns
80
+ invalidated = revoked_count.positive? && PermissionForge::Cache::Failure.none? { @user.invalidate_permissions }
81
+
82
+ { revoked_sets: revoked_count, invalidated: invalidated }
83
+ end
84
+
85
+ def revoke_and_record(affected_assignments)
86
+ assignments = affected_assignments.includes(:permission_set).to_a
87
+ revoked = assignments.select { |assignment| assignment.destroy.present? }
88
+ record_summary(assignments, revoked)
89
+ revoked.size
90
+ end
91
+
92
+ # One event, or one per tenant plus the totals (record_per_tenant).
93
+ # Each names the sets of its assignments and counts the revoked ones.
94
+ def record_summary(assignments, revoked)
95
+ record_per_tenant('consent.withdrawn', assignments, subject_user_id: @user.id) do |rows|
96
+ summary_data(rows.filter_map(&:permission_set), rows.count { |row| revoked.include?(row) })
97
+ end
98
+ end
99
+
100
+ def summary_data(sets, revoked_count)
101
+ {
102
+ 'operation' => 'consent_withdrawal',
103
+ 'consent_flag' => @consent_flag.to_s,
104
+ 'subject_user_id' => @user.id.to_s,
105
+ 'set_uuids' => sets.map(&:uuid).uniq,
106
+ 'set_names' => sets.map(&:name).uniq,
107
+ 'revoked' => revoked_count,
108
+ 'dry_run' => false
109
+ }
110
+ end
111
+
112
+ def validate!
113
+ raise ArgumentError, 'user is required' if @user.nil?
114
+ raise ArgumentError, 'consent_flag is required' if @consent_flag.blank?
115
+ end
116
+
117
+ def consent_gated_sets
118
+ PermissionForge::PermissionSet.where(requires_session_flag: @consent_flag)
119
+ end
120
+
121
+ def user_assignments_for(sets)
122
+ PermissionForge::PermissionSetUser.where(user: @user, permission_set: sets)
123
+ end
124
+
125
+ def dry_run_result(sets, assignments)
126
+ {
127
+ revoked_sets: 0,
128
+ affected_sets: sets.pluck(:name),
129
+ affected_assignment_count: assignments.count,
130
+ dry_run: true
131
+ }
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,43 @@
1
+ module PermissionForge
2
+ module Services
3
+ # Aggregates dashboard statistics from permission models.
4
+ # Extracted from DashboardController for SRP.
5
+ #
6
+ # @api private
7
+ class DashboardStats
8
+ def self.call
9
+ {}.merge(permission_set_stats).merge(model_counts).merge(recent_records)
10
+ end
11
+
12
+ class << self
13
+ private
14
+
15
+ def permission_set_stats
16
+ {
17
+ permission_sets_count: PermissionForge::PermissionSet.count,
18
+ active_sets_count: PermissionForge::PermissionSet.active.count,
19
+ muting_sets_count: PermissionForge::PermissionSet.muting.count,
20
+ conditional_sets_count: PermissionForge::PermissionSet.conditional.count,
21
+ expired_assignments_count: PermissionForge::PermissionSetUser.expired.count
22
+ }
23
+ end
24
+
25
+ def model_counts
26
+ {
27
+ resources_count: PermissionForge::Resource.count,
28
+ rules_count: PermissionForge::PermissionSetRule.count,
29
+ groups_count: PermissionForge::PermissionSetGroup.count,
30
+ users_count: PermissionForge::PermissionSetUser.select(:user_id).distinct.count
31
+ }
32
+ end
33
+
34
+ def recent_records
35
+ {
36
+ recent_permission_sets: PermissionForge::PermissionSet.order(created_at: :desc).limit(5),
37
+ recent_resources: PermissionForge::Resource.order(created_at: :desc).limit(5)
38
+ }
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,306 @@
1
+ module PermissionForge
2
+ module Services
3
+ # Supports a GDPR Art. 17 (right to erasure) request for a host user.
4
+ # What it removes:
5
+ # - the person's permission set assignments and group memberships, in
6
+ # the ambient tenant, each through its callbacks (one audit event per
7
+ # row);
8
+ # - the person's cached permissions;
9
+ # - the person's name labels in the audit log (actor_label on events
10
+ # they acted in, record_label on events about them): with a tenant
11
+ # current, in that tenant's events; with none (the rake task, a
12
+ # platform operator), in every tenant's, events without a tenant
13
+ # included;
14
+ # - whatever the host's `on_erasure` callback deletes.
15
+ # What remains, so the log still says what happened:
16
+ # - every audit event keeps its ids (actor_id, subject_user_id), ip,
17
+ # request_id, reason and data (snapshots of the removed rows);
18
+ # - created_by_id / updated_by_id / reviewed_by_id /
19
+ # sensitive_acknowledged_by_id on rows the person created, edited or
20
+ # reviewed;
21
+ # - assignments and memberships in other tenants when a tenant is
22
+ # current (counted in `assignments_remaining_elsewhere` and
23
+ # `group_assignments_remaining_elsewhere`);
24
+ # - the person's own record and data in the host app, unless
25
+ # `on_erasure` deletes them.
26
+ #
27
+ # @example Full erasure
28
+ # result = DataSubjectErasure.call(user: user)
29
+ # # => { assignments_removed: 3, group_assignments_removed: 1, cache_cleared: true, callback_invoked: true }
30
+ #
31
+ # `cache_cleared` is false when the cache store did not confirm the
32
+ # removal (it is unreachable, say): the assignments are gone, but the
33
+ # user's cached permissions may live until cache_ttl. The failure is also
34
+ # reported through `config.on_cache_failure`. It is nil when the service
35
+ # is called inside a database transaction: the assignments are only gone
36
+ # for everybody else — and their cached permissions with them — once that
37
+ # transaction has committed, and a failure then is reported through
38
+ # `config.on_cache_failure` alone.
39
+ #
40
+ # @example Dry run (inspect without removing)
41
+ # result = DataSubjectErasure.call(user: user, dry_run: true)
42
+ # # => { assignments_count: 3, group_assignments_count: 1, permission_sets: [...], dry_run: true }
43
+ #
44
+ # The audit log (docs/audit-log.md): the rows are destroyed through their
45
+ # callbacks, so each assignment writes its own `assignment.revoked`
46
+ # event; then the personal labels the log holds about the person are
47
+ # cleared and ONE `erasure.performed` event is written. `actor:` (a user,
48
+ # an Audit::Actor or a label String; nil means the actor of the context)
49
+ # and `reason:` say who ran it and why. A dry run writes nothing at all.
50
+ #
51
+ # The row removal, the label scrub and `erasure.performed` happen in ONE
52
+ # transaction: if the summary cannot be written under
53
+ # audit_change_failure_strategy :raise, nothing changed. Under :report or
54
+ # :log the host chose that changes go through when their record fails: the
55
+ # erasure completes without `erasure.performed` (the failure is reported) —
56
+ # the right to erasure is not blocked by the audit write. The cache
57
+ # invalidation and `on_erasure` run after it. Events a callback writes
58
+ # afterwards keep the labels they were given, so a host callback must not
59
+ # create assignments for the erased person.
60
+ #
61
+ # `on_erasure` runs once the erasure is committed for everybody:
62
+ # - called outside a transaction, right after the erasure; if it
63
+ # raises, the erasure stands and is recorded, and the error propagates
64
+ # (`callback_invoked` is true);
65
+ # - called inside the caller's transaction, after that transaction has
66
+ # committed (OpenTransaction#after_commit), and never if it rolls
67
+ # back (`callback_invoked` is nil: only known at the commit). An
68
+ # error there is logged and reported (Rails.error, `handled: true`,
69
+ # like every after-commit error the gem catches), not raised: raising
70
+ # from Rails' commit would skip the after-commit work queued behind
71
+ # it, the cache invalidations of the caller's other changes included.
72
+ #
73
+ # With no tenant ambient the service runs over every tenant also under
74
+ # acts_as_tenant's require_tenant (Concerns::AcrossTenants).
75
+ #
76
+ # @api public
77
+ class DataSubjectErasure
78
+ include Concerns::AuditedOperation
79
+ include Concerns::AcrossTenants
80
+
81
+ # Erases a user's permission data (GDPR Art. 17).
82
+ # @return [Hash] see {#call}
83
+ def self.call(...) = new(...).call
84
+
85
+ def initialize(user:, on_erasure: nil, dry_run: false, actor: nil, reason: nil)
86
+ @user = user
87
+ @on_erasure = on_erasure
88
+ @dry_run = dry_run
89
+ @actor = actor
90
+ @reason = reason
91
+ end
92
+
93
+ # @return [Hash] the counts of what was removed and what remains
94
+ def call
95
+ validate!
96
+
97
+ across_tenants do
98
+ next dry_run_result if @dry_run
99
+
100
+ audited(actor: erased_person_unnamed, reason: @reason) { erase }
101
+ end
102
+ end
103
+
104
+ private
105
+
106
+ def erase
107
+ removed = ActiveRecord::Base.transaction { remove_and_record }
108
+ cache_cleared = clear_cache
109
+
110
+ removed.merge(cache_cleared: cache_cleared, callback_invoked: run_callback)
111
+ end
112
+
113
+ def remove_and_record
114
+ sets = permission_set_assignments.includes(:permission_set).filter_map(&:permission_set)
115
+ groups = group_assignments.includes(:permission_set_group).filter_map(&:permission_set_group)
116
+
117
+ counts = { assignments_removed: remove_permission_set_assignments,
118
+ group_assignments_removed: remove_group_assignments }
119
+ counts.merge!(remaining_elsewhere)
120
+ scrubbed = scrub_labels
121
+ record_summary(sets, groups, counts, scrubbed)
122
+ counts
123
+ end
124
+
125
+ # What the person still has in other tenants (nothing is removed there).
126
+ def remaining_elsewhere
127
+ {
128
+ assignments_remaining_elsewhere: PermissionForge::PermissionSetUser.unscoped.where(user: @user).count,
129
+ group_assignments_remaining_elsewhere: PermissionForge::PermissionSetGroupUser.unscoped.where(user: @user).count
130
+ }
131
+ end
132
+
133
+ # A person who erases themselves must not leave their own name on the
134
+ # events of the erasure: the id stays, the label does not. Without
135
+ # `actor:` the context's actor (the admin of the request) is the one
136
+ # compared.
137
+ def erased_person_unnamed
138
+ resolved = audit_actor_for(@actor) || PermissionForge::Audit::Actor.current
139
+ return @actor unless resolved.type == 'user' && resolved.id == @user.id.to_s
140
+
141
+ PermissionForge::Audit::Actor.new(type: resolved.type, id: resolved.id)
142
+ end
143
+
144
+ # The only sanctioned mutation of audit events: the snapshots of the
145
+ # person's name. Every id stays, so the trail still says WHAT happened;
146
+ # only WHO (as a name) is forgotten. Past the insert-only guards on
147
+ # purpose (`update_all` is the one path T1 leaves open), in the events
148
+ # of scrubbed_events, only for actors of type 'user' (an API key or a
149
+ # basic auth login that shares the id is somebody else), and recorded
150
+ # in `erasure.performed`.
151
+ def scrub_labels
152
+ events = scrubbed_events
153
+ id = @user.id.to_s
154
+ {
155
+ actor_labels: events.where(actor_type: 'user', actor_id: id).where.not(actor_label: nil).update_all(actor_label: nil),
156
+ record_labels: events.where(subject_user_id: id).where.not(record_label: nil).update_all(record_label: nil)
157
+ }
158
+ end
159
+
160
+ # With a tenant current (a tenant admin), that tenant's events only: a
161
+ # tenant cannot reach another tenant's log. With none (the rake task, a
162
+ # platform operator), every tenant's, and the events without a tenant.
163
+ # Built on `unscoped` and an explicit tenant condition, so it does not
164
+ # depend on acts_as_tenant's default scope. An events table without the
165
+ # tenant column cannot be told apart by tenant: every event.
166
+ def scrubbed_events
167
+ PermissionForge::AuditEvent.unscoped.in_tenant(PermissionForge.ambient_tenant_id)
168
+ end
169
+
170
+ def record_summary(sets, groups, counts, scrubbed)
171
+ PermissionForge::Audit.record('erasure.performed', subject_user_id: @user.id,
172
+ data: summary_data(sets, groups, counts, scrubbed))
173
+ end
174
+
175
+ def summary_data(sets, groups, counts, scrubbed)
176
+ cleared = { 'events_scrubbed' => scrubbed.values.sum, 'actor_labels_cleared' => scrubbed[:actor_labels],
177
+ 'record_labels_cleared' => scrubbed[:record_labels] }
178
+ names(sets, 'set').merge(names(groups, 'group'), counts.transform_keys(&:to_s), cleared,
179
+ 'operation' => 'erasure', 'subject_user_id' => @user.id.to_s,
180
+ 'callback_invoked' => !@on_erasure.nil?, 'dry_run' => false)
181
+ end
182
+
183
+ def names(records, kind)
184
+ { "#{kind}_uuids" => records.map(&:uuid).uniq, "#{kind}_names" => records.map(&:name).uniq }
185
+ end
186
+
187
+ def validate!
188
+ raise ArgumentError, 'user is required' if @user.nil?
189
+ end
190
+
191
+ def permission_set_assignments
192
+ PermissionForge::PermissionSetUser.where(user: @user)
193
+ end
194
+
195
+ def group_assignments
196
+ PermissionForge::PermissionSetGroupUser.where(user: @user)
197
+ end
198
+
199
+ def remove_permission_set_assignments
200
+ records = permission_set_assignments
201
+ count = records.count
202
+ records.destroy_all
203
+ count
204
+ end
205
+
206
+ def remove_group_assignments
207
+ records = group_assignments
208
+ count = records.count
209
+ records.destroy_all
210
+ count
211
+ end
212
+
213
+ # Asks the gem's own failure bookkeeping, not the value of
214
+ # invalidate_permissions: a host app overrides that method.
215
+ #
216
+ # An invalidation leaves the per-context variants of a user with
217
+ # conditional sets to their TTL (unreachable). They hold the profile,
218
+ # so they are removed as well (Cache::VariantRemoval) — inside the
219
+ # caller's transaction once it has committed, after the assignments'
220
+ # own invalidations: until then another request may still cache one
221
+ # from the rows as they were.
222
+ #
223
+ # @return [Boolean, nil] whether the store confirmed the removal; nil
224
+ # inside an open transaction, where that is only known at its commit
225
+ def clear_cache
226
+ transaction = PermissionForge::OpenTransaction.current
227
+ cleared = PermissionForge::Cache::Failure.none? do
228
+ invalidate_user
229
+ PermissionForge::Cache::VariantRemoval.call(**variant_removal) unless transaction
230
+ end
231
+ return cleared unless transaction
232
+
233
+ remove_variants_after_commit(transaction)
234
+ nil
235
+ end
236
+
237
+ # The arguments of the person's Cache::VariantRemoval: the keys the
238
+ # instance names, as invalidate_permissions removes them, every slot of
239
+ # each without reading the index entries (a read that fails silently
240
+ # and an evicted index entry must not leave a variant behind).
241
+ def variant_removal
242
+ named = @user.respond_to?(:permission_cache_key) ? { ambient_key: @user.permission_cache_key } : {}
243
+ { user_ids: [@user.id], indexed_only: false, **named }
244
+ end
245
+
246
+ # Nothing raises out of the commit: a failure is reported, like every
247
+ # after-commit invalidation (Cache::Failure).
248
+ def remove_variants_after_commit(transaction)
249
+ removal = variant_removal
250
+ transaction.after_commit do
251
+ PermissionForge::Cache::VariantRemoval.call(**removal)
252
+ rescue StandardError => e
253
+ PermissionForge::Cache::Failure.report(operation: :invalidate, user_id: removal[:user_ids].first, error: e)
254
+ end
255
+ end
256
+
257
+ def invalidate_user
258
+ # invalidate_permissions (not a raw cache delete) so the ambient and
259
+ # tenant-agnostic keys are both removed AND the process-wide memo
260
+ # version bumps — no live instance may keep serving an erased user's
261
+ # permissions from its instance memo.
262
+ return @user.invalidate_permissions if @user.respond_to?(:invalidate_permissions)
263
+
264
+ cache = PermissionForge.cache
265
+ key = @user.permission_cache_key
266
+ PermissionForge::Cache::Failure.report(operation: :invalidate, keys: [key]) unless cache.expunge(key)
267
+ PermissionForge.bump_permissions_cache_version!
268
+ end
269
+
270
+ # @return [Boolean, nil] false without a callback; true once it ran;
271
+ # nil when it waits for the caller's transaction to commit
272
+ def run_callback
273
+ return false if @on_erasure.nil?
274
+
275
+ transaction = PermissionForge::OpenTransaction.current
276
+ return defer_callback(transaction) if transaction
277
+
278
+ @on_erasure.call(@user)
279
+ true
280
+ end
281
+
282
+ def defer_callback(transaction)
283
+ callback = @on_erasure
284
+ user = @user
285
+ transaction.after_commit { report_callback_failure { callback.call(user) } }
286
+ nil
287
+ end
288
+
289
+ def report_callback_failure
290
+ yield
291
+ rescue StandardError => e
292
+ Rails.logger&.error("[PermissionForge] on_erasure failed after the erasure committed — #{e.class}: #{e.message}")
293
+ Rails.error.report(e, handled: true, source: 'permission_forge.data_subject_erasure')
294
+ end
295
+
296
+ def dry_run_result
297
+ {
298
+ assignments_count: permission_set_assignments.count,
299
+ group_assignments_count: group_assignments.count,
300
+ permission_sets: permission_set_assignments.includes(:permission_set).filter_map { |psu| psu.permission_set&.name },
301
+ dry_run: true
302
+ }
303
+ end
304
+ end
305
+ end
306
+ end