permission_forge 0.14.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (347) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +2372 -0
  3. data/README.md +76 -25
  4. data/app/controllers/concerns/permission_forge/api/crud_actions.rb +3 -2
  5. data/app/controllers/concerns/permission_forge/api/input_shapes.rb +200 -0
  6. data/app/controllers/concerns/permission_forge/api/pagination.rb +12 -5
  7. data/app/controllers/concerns/permission_forge/api/response_renderer.rb +37 -1
  8. data/app/controllers/concerns/permission_forge/api_authentication.rb +13 -2
  9. data/app/controllers/concerns/permission_forge/cache_confirmation.rb +42 -11
  10. data/app/controllers/concerns/permission_forge/change_review.rb +42 -0
  11. data/app/controllers/concerns/permission_forge/copy_refusal.rb +39 -0
  12. data/app/controllers/concerns/permission_forge/param_shapes.rb +207 -7
  13. data/app/controllers/concerns/permission_forge/save_notice.rb +69 -0
  14. data/app/controllers/permission_forge/api/audit_events_controller.rb +1 -1
  15. data/app/controllers/permission_forge/api/base_controller.rb +163 -12
  16. data/app/controllers/permission_forge/api/dashboard_controller.rb +29 -4
  17. data/app/controllers/permission_forge/api/permission_set_group_users_controller.rb +5 -2
  18. data/app/controllers/permission_forge/api/permission_set_groups_controller.rb +5 -4
  19. data/app/controllers/permission_forge/api/permission_set_rules_controller.rb +7 -5
  20. data/app/controllers/permission_forge/api/permission_set_users_controller.rb +80 -19
  21. data/app/controllers/permission_forge/api/permission_sets_controller.rb +12 -6
  22. data/app/controllers/permission_forge/api/resource_actions_controller.rb +1 -1
  23. data/app/controllers/permission_forge/api/resource_fields_controller.rb +3 -4
  24. data/app/controllers/permission_forge/api/resources_controller.rb +26 -12
  25. data/app/controllers/permission_forge/api/unmatched_routes_controller.rb +20 -0
  26. data/app/controllers/permission_forge/application_controller.rb +39 -5
  27. data/app/controllers/permission_forge/assignments_controller.rb +36 -34
  28. data/app/controllers/permission_forge/bulk_assignments_controller.rb +8 -4
  29. data/app/controllers/permission_forge/field_lookups_controller.rb +1 -1
  30. data/app/controllers/permission_forge/maintenance_controller.rb +6 -2
  31. data/app/controllers/permission_forge/people_controller.rb +14 -14
  32. data/app/controllers/permission_forge/permission_set_editor.rb +20 -6
  33. data/app/controllers/permission_forge/permission_set_group_users_controller.rb +3 -1
  34. data/app/controllers/permission_forge/permission_set_groups_controller.rb +23 -9
  35. data/app/controllers/permission_forge/permission_set_rules_controller.rb +20 -10
  36. data/app/controllers/permission_forge/permission_sets_controller.rb +23 -24
  37. data/app/controllers/permission_forge/resource_actions_controller.rb +11 -6
  38. data/app/controllers/permission_forge/resource_fields_controller.rb +37 -19
  39. data/app/controllers/permission_forge/resources_controller.rb +12 -11
  40. data/app/frontend/permission_forge/pf_admin.css +99 -17
  41. data/app/frontend/permission_forge/pf_admin.js +36 -0
  42. data/app/helpers/permission_forge/application_helper.rb +22 -72
  43. data/app/helpers/permission_forge/audit_attribution_helper.rb +77 -0
  44. data/app/helpers/permission_forge/audit_helper.rb +57 -119
  45. data/app/helpers/permission_forge/audit_records_helper.rb +103 -0
  46. data/app/helpers/permission_forge/audit_sentences_helper.rb +56 -2
  47. data/app/helpers/permission_forge/badges_helper.rb +74 -17
  48. data/app/helpers/permission_forge/changes_helper.rb +24 -6
  49. data/app/helpers/permission_forge/conditions_helper.rb +119 -0
  50. data/app/helpers/permission_forge/labels_helper.rb +134 -0
  51. data/app/helpers/permission_forge/people_helper.rb +11 -19
  52. data/app/helpers/permission_forge/rule_problems_helper.rb +73 -0
  53. data/app/helpers/permission_forge/sentences_helper.rb +119 -98
  54. data/app/helpers/permission_forge/settings_helper.rb +2 -2
  55. data/app/helpers/permission_forge/ui_helper.rb +65 -4
  56. data/app/models/permission_forge/audit_event.rb +5 -1
  57. data/app/models/permission_forge/permission_set.rb +208 -18
  58. data/app/models/permission_forge/permission_set_group.rb +10 -4
  59. data/app/models/permission_forge/permission_set_group_user.rb +3 -1
  60. data/app/models/permission_forge/permission_set_rule.rb +254 -17
  61. data/app/models/permission_forge/permission_set_user.rb +38 -3
  62. data/app/models/permission_forge/resource.rb +18 -8
  63. data/app/models/permission_forge/resource_action.rb +15 -6
  64. data/app/models/permission_forge/resource_field.rb +97 -11
  65. data/app/views/permission_forge/assignments/confirm_destroy.html.erb +4 -4
  66. data/app/views/permission_forge/assignments/new.html.erb +10 -6
  67. data/app/views/permission_forge/audit_events/index.html.erb +1 -1
  68. data/app/views/permission_forge/audit_events/show.html.erb +16 -9
  69. data/app/views/permission_forge/bulk_assignments/confirm_revoke.html.erb +3 -3
  70. data/app/views/permission_forge/bulk_assignments/new.html.erb +3 -3
  71. data/app/views/permission_forge/field_lookups/show.html.erb +2 -2
  72. data/app/views/permission_forge/home/_broken_rules_card.html.erb +35 -0
  73. data/app/views/permission_forge/home/_conditions_card.html.erb +28 -0
  74. data/app/views/permission_forge/home/_ending_card.html.erb +11 -5
  75. data/app/views/permission_forge/home/_expired_card.html.erb +1 -1
  76. data/app/views/permission_forge/home/_mask_strategies_card.html.erb +27 -0
  77. data/app/views/permission_forge/home/_privacy_card.html.erb +21 -7
  78. data/app/views/permission_forge/home/_rules_card.html.erb +1 -1
  79. data/app/views/permission_forge/home/_secret_rules_card.html.erb +33 -0
  80. data/app/views/permission_forge/home/_stale_card.html.erb +7 -5
  81. data/app/views/permission_forge/home/_tidy_card.html.erb +1 -1
  82. data/app/views/permission_forge/home/index.html.erb +5 -6
  83. data/app/views/permission_forge/people/_access_tab.html.erb +26 -19
  84. data/app/views/permission_forge/people/_audit_tab.html.erb +8 -8
  85. data/app/views/permission_forge/people/_effective_field_source.html.erb +13 -7
  86. data/app/views/permission_forge/people/_effective_tab.html.erb +16 -15
  87. data/app/views/permission_forge/people/_effective_why.html.erb +20 -16
  88. data/app/views/permission_forge/people/_why_item.html.erb +1 -1
  89. data/app/views/permission_forge/people/confirm_remove_all.html.erb +3 -3
  90. data/app/views/permission_forge/people/erasure_preview.html.erb +1 -1
  91. data/app/views/permission_forge/permission_set_groups/confirm_destroy.html.erb +3 -3
  92. data/app/views/permission_forge/permission_set_groups/index.html.erb +3 -3
  93. data/app/views/permission_forge/permission_set_groups/show.html.erb +28 -19
  94. data/app/views/permission_forge/permission_set_rules/_builder.html.erb +35 -15
  95. data/app/views/permission_forge/permission_set_rules/_preview.html.erb +17 -0
  96. data/app/views/permission_forge/permission_set_rules/confirm_destroy.html.erb +3 -3
  97. data/app/views/permission_forge/permission_set_rules/preview.html.erb +12 -0
  98. data/app/views/permission_forge/permission_sets/_assignments_tab.html.erb +37 -22
  99. data/app/views/permission_forge/permission_sets/_audit_tab.html.erb +4 -4
  100. data/app/views/permission_forge/permission_sets/_condition_problems.html.erb +15 -0
  101. data/app/views/permission_forge/permission_sets/_conditions_tab.html.erb +14 -8
  102. data/app/views/permission_forge/permission_sets/_header.html.erb +6 -5
  103. data/app/views/permission_forge/permission_sets/_records_summary.html.erb +2 -2
  104. data/app/views/permission_forge/permission_sets/_resources_tab.html.erb +4 -4
  105. data/app/views/permission_forge/permission_sets/confirm_destroy.html.erb +4 -4
  106. data/app/views/permission_forge/permission_sets/confirm_switch_mode.html.erb +3 -3
  107. data/app/views/permission_forge/permission_sets/index.html.erb +6 -6
  108. data/app/views/permission_forge/permission_sets/new.html.erb +2 -2
  109. data/app/views/permission_forge/permission_sets/review.html.erb +13 -3
  110. data/app/views/permission_forge/permission_sets/show.html.erb +7 -5
  111. data/app/views/permission_forge/resource_actions/_across_sets.html.erb +7 -10
  112. data/app/views/permission_forge/resource_actions/_effect.html.erb +5 -5
  113. data/app/views/permission_forge/resource_actions/_form.html.erb +1 -1
  114. data/app/views/permission_forge/resource_actions/_managed_note.html.erb +5 -3
  115. data/app/views/permission_forge/resource_actions/confirm_destroy.html.erb +1 -1
  116. data/app/views/permission_forge/resource_actions/new.html.erb +1 -1
  117. data/app/views/permission_forge/resource_actions/show.html.erb +2 -2
  118. data/app/views/permission_forge/resource_fields/_explain.html.erb +19 -15
  119. data/app/views/permission_forge/resource_fields/show.html.erb +21 -12
  120. data/app/views/permission_forge/resources/_actions_tab.html.erb +4 -4
  121. data/app/views/permission_forge/resources/_fields_tab.html.erb +17 -9
  122. data/app/views/permission_forge/resources/_rules_tab.html.erb +9 -6
  123. data/app/views/permission_forge/resources/_sync.html.erb +8 -5
  124. data/app/views/permission_forge/resources/confirm_destroy.html.erb +3 -3
  125. data/app/views/permission_forge/resources/new.html.erb +3 -2
  126. data/app/views/permission_forge/resources/show.html.erb +6 -6
  127. data/app/views/permission_forge/settings/_row.html.erb +2 -2
  128. data/app/views/permission_forge/settings/show.html.erb +10 -1
  129. data/app/views/permission_forge/shared/_across_sets.html.erb +8 -9
  130. data/app/views/permission_forge/shared/_audit_events.html.erb +2 -2
  131. data/app/views/permission_forge/shared/_audit_footer.html.erb +13 -2
  132. data/app/views/permission_forge/shared/_crud_cell.html.erb +6 -6
  133. data/app/views/permission_forge/shared/_mode_banners.html.erb +1 -1
  134. data/app/views/permission_forge/shared/_savebar.html.erb +4 -2
  135. data/config/locales/permission_forge.en.yml +480 -69
  136. data/config/locales/permission_forge.mk.yml +499 -79
  137. data/config/locales/permission_forge.sq.yml +509 -80
  138. data/config/routes.rb +38 -23
  139. data/lib/generators/permission_forge/install/install_generator.rb +21 -0
  140. data/lib/generators/permission_forge/install/templates/initializer.rb.tt +35 -34
  141. data/lib/generators/permission_forge/install/templates/migration.rb.tt +5 -5
  142. data/lib/permission_forge/activation_conditions.rb +200 -0
  143. data/lib/permission_forge/admin/action_label.rb +33 -0
  144. data/lib/permission_forge/admin/action_rows.rb +3 -1
  145. data/lib/permission_forge/admin/actor_label.rb +52 -0
  146. data/lib/permission_forge/admin/attention_items.rb +152 -31
  147. data/lib/permission_forge/admin/audit_log.rb +4 -7
  148. data/lib/permission_forge/admin/audit_trail.rb +33 -17
  149. data/lib/permission_forge/admin/bulk_people_matcher.rb +32 -14
  150. data/lib/permission_forge/admin/chain_entries.rb +42 -0
  151. data/lib/permission_forge/admin/change_set.rb +12 -1
  152. data/lib/permission_forge/admin/change_set_parts/base.rb +5 -4
  153. data/lib/permission_forge/admin/change_set_parts/context.rb +3 -1
  154. data/lib/permission_forge/admin/change_set_parts/field_edit.rb +4 -1
  155. data/lib/permission_forge/admin/change_set_parts/field_levels.rb +1 -1
  156. data/lib/permission_forge/admin/change_set_parts/new_actions.rb +1 -1
  157. data/lib/permission_forge/admin/change_set_parts/new_fields.rb +38 -11
  158. data/lib/permission_forge/admin/change_set_parts/resource_edit.rb +17 -1
  159. data/lib/permission_forge/admin/change_set_parts/resource_flags.rb +1 -1
  160. data/lib/permission_forge/admin/change_set_parts/set_attrs.rb +4 -0
  161. data/lib/permission_forge/admin/change_set_parts/values.rb +10 -4
  162. data/lib/permission_forge/admin/column_fields.rb +22 -21
  163. data/lib/permission_forge/admin/conditions_form.rb +47 -15
  164. data/lib/permission_forge/admin/controller_catalog.rb +42 -14
  165. data/lib/permission_forge/admin/editable_lock.rb +83 -3
  166. data/lib/permission_forge/admin/effective_access.rb +147 -48
  167. data/lib/permission_forge/admin/effective_fields.rb +75 -67
  168. data/lib/permission_forge/admin/error_sentence.rb +47 -0
  169. data/lib/permission_forge/admin/expiry_presets.rb +17 -0
  170. data/lib/permission_forge/admin/exports/holdings.rb +6 -3
  171. data/lib/permission_forge/admin/field_across_sets.rb +7 -4
  172. data/lib/permission_forge/admin/field_chain.rb +109 -0
  173. data/lib/permission_forge/admin/field_detail.rb +24 -11
  174. data/lib/permission_forge/admin/field_label.rb +109 -0
  175. data/lib/permission_forge/admin/field_level.rb +4 -68
  176. data/lib/permission_forge/admin/field_provenance.rb +132 -0
  177. data/lib/permission_forge/admin/field_rows.rb +26 -11
  178. data/lib/permission_forge/admin/last_changes.rb +14 -0
  179. data/lib/permission_forge/admin/list_sentence.rb +32 -0
  180. data/lib/permission_forge/admin/mask_strategy_choices.rb +57 -0
  181. data/lib/permission_forge/admin/model_catalog.rb +83 -39
  182. data/lib/permission_forge/admin/model_label.rb +66 -0
  183. data/lib/permission_forge/admin/people_candidates.rb +21 -9
  184. data/lib/permission_forge/admin/people_directory.rb +23 -5
  185. data/lib/permission_forge/admin/permission_set_factory.rb +6 -15
  186. data/lib/permission_forge/admin/person_label.rb +105 -16
  187. data/lib/permission_forge/admin/prefill.rb +92 -0
  188. data/lib/permission_forge/admin/record_name.rb +45 -0
  189. data/lib/permission_forge/admin/rule_builder.rb +77 -3
  190. data/lib/permission_forge/admin/rule_form.rb +44 -10
  191. data/lib/permission_forge/admin/rule_form_input.rb +27 -11
  192. data/lib/permission_forge/admin/rule_notes.rb +43 -0
  193. data/lib/permission_forge/admin/rule_preview.rb +96 -0
  194. data/lib/permission_forge/admin/sensitive_columns.rb +10 -62
  195. data/lib/permission_forge/admin/settings_catalog.rb +64 -12
  196. data/lib/permission_forge/admin/state_label.rb +97 -0
  197. data/lib/permission_forge/admin_method_override.rb +44 -0
  198. data/lib/permission_forge/api/schema.rb +322 -0
  199. data/lib/permission_forge/api/serializers/base_serializer.rb +87 -1
  200. data/lib/permission_forge/api/serializers/permission_set_group_user_serializer.rb +1 -1
  201. data/lib/permission_forge/api/serializers/permission_set_rule_serializer.rb +1 -1
  202. data/lib/permission_forge/api/serializers/permission_set_serializer.rb +5 -2
  203. data/lib/permission_forge/api/serializers/permission_set_user_serializer.rb +1 -1
  204. data/lib/permission_forge/api/serializers/resource_action_serializer.rb +1 -1
  205. data/lib/permission_forge/api/serializers/resource_field_serializer.rb +1 -1
  206. data/lib/permission_forge/api/serializers/resource_serializer.rb +1 -1
  207. data/lib/permission_forge/api_authorizations.rb +59 -0
  208. data/lib/permission_forge/api_only_admin.rb +81 -0
  209. data/lib/permission_forge/audit/actor_codes.rb +32 -0
  210. data/lib/permission_forge/audit/actor_resolver.rb +1 -2
  211. data/lib/permission_forge/audit/boot_warnings.rb +29 -9
  212. data/lib/permission_forge/audit/event_catalog.rb +16 -6
  213. data/lib/permission_forge/audit/event_query.rb +3 -3
  214. data/lib/permission_forge/audit/reason_codes.rb +25 -0
  215. data/lib/permission_forge/audit/runtime.rb +9 -2
  216. data/lib/permission_forge/audit/settings_observer.rb +2 -2
  217. data/lib/permission_forge/audit/unmasked_without_user.rb +169 -0
  218. data/lib/permission_forge/audit.rb +24 -10
  219. data/lib/permission_forge/boot_checks.rb +180 -63
  220. data/lib/permission_forge/cache/base.rb +10 -0
  221. data/lib/permission_forge/cache/null_cache.rb +5 -0
  222. data/lib/permission_forge/cache/outcome.rb +119 -0
  223. data/lib/permission_forge/cache/rails_cache.rb +6 -0
  224. data/lib/permission_forge/cache/report.rb +150 -0
  225. data/lib/permission_forge/cache/user_keys.rb +8 -4
  226. data/lib/permission_forge/cache_warnings.rb +140 -0
  227. data/lib/permission_forge/concerns/auto_permission_scoping.rb +19 -2
  228. data/lib/permission_forge/concerns/has_tenant_scoping.rb +1 -1
  229. data/lib/permission_forge/concerns/has_uuid.rb +1 -1
  230. data/lib/permission_forge/concerns/keeps_copied_values.rb +48 -0
  231. data/lib/permission_forge/concerns/masked_attributes/alias_access.rb +12 -2
  232. data/lib/permission_forge/concerns/masked_attributes/virtual_readers.rb +6 -4
  233. data/lib/permission_forge/concerns/masked_attributes.rb +150 -34
  234. data/lib/permission_forge/concerns/normalizes_label.rb +78 -0
  235. data/lib/permission_forge/concerns/permission_cache_accessor.rb +3 -3
  236. data/lib/permission_forge/concerns/permission_scoped_findable.rb +121 -29
  237. data/lib/permission_forge/concerns/permissioned_user.rb +1 -1
  238. data/lib/permission_forge/concerns/records_audit_events.rb +13 -6
  239. data/lib/permission_forge/concerns/requires_name.rb +37 -0
  240. data/lib/permission_forge/concerns/validates_column_lengths.rb +24 -0
  241. data/lib/permission_forge/concerns/validates_dependency.rb +5 -2
  242. data/lib/permission_forge/concerns/validates_required_columns.rb +74 -0
  243. data/lib/permission_forge/conditional_sets.rb +13 -6
  244. data/lib/permission_forge/configuration/admin_ui_settings.rb +18 -4
  245. data/lib/permission_forge/configuration/audit_settings.rb +2 -1
  246. data/lib/permission_forge/configuration/controller_action_settings.rb +5 -4
  247. data/lib/permission_forge/configuration/masked_attributes_settings.rb +73 -19
  248. data/lib/permission_forge/configuration/presets.rb +20 -11
  249. data/lib/permission_forge/configuration/query_scoping_settings.rb +55 -0
  250. data/lib/permission_forge/configuration/strategy_values.rb +2 -1
  251. data/lib/permission_forge/configuration.rb +41 -18
  252. data/lib/permission_forge/controller_actions/managed_index.rb +8 -7
  253. data/lib/permission_forge/database_reach.rb +93 -0
  254. data/lib/permission_forge/deprecation.rb +1 -1
  255. data/lib/permission_forge/docs/api_samples.rb +250 -0
  256. data/lib/permission_forge/docs/configuration_reference.rb +172 -0
  257. data/lib/permission_forge/encrypted_attributes.rb +150 -0
  258. data/lib/permission_forge/engine.rb +46 -14
  259. data/lib/permission_forge/execution_state/restore.rb +74 -0
  260. data/lib/permission_forge/execution_state.rb +8 -1
  261. data/lib/permission_forge/field_level.rb +123 -0
  262. data/lib/permission_forge/field_names.rb +145 -0
  263. data/lib/permission_forge/field_variants.rb +96 -0
  264. data/lib/permission_forge/generated_text.rb +95 -0
  265. data/lib/permission_forge/inspection_filters.rb +83 -0
  266. data/lib/permission_forge/managed_fields.rb +66 -0
  267. data/lib/permission_forge/masked_models.rb +25 -19
  268. data/lib/permission_forge/masking_sync.rb +100 -25
  269. data/lib/permission_forge/model_bindings.rb +30 -19
  270. data/lib/permission_forge/model_lookup.rb +42 -0
  271. data/lib/permission_forge/operators/encrypted_alias.rb +67 -0
  272. data/lib/permission_forge/operators/registry.rb +6 -0
  273. data/lib/permission_forge/operators/rule_column.rb +242 -27
  274. data/lib/permission_forge/operators/rule_path.rb +179 -0
  275. data/lib/permission_forge/operators/rule_problem.rb +309 -0
  276. data/lib/permission_forge/operators/rule_sources.rb +96 -0
  277. data/lib/permission_forge/operators/value_casters/postgresql_types.rb +237 -0
  278. data/lib/permission_forge/operators/value_casters/temporal.rb +9 -20
  279. data/lib/permission_forge/operators/value_casters.rb +61 -13
  280. data/lib/permission_forge/operators/value_typing.rb +83 -7
  281. data/lib/permission_forge/query_scoping_warnings.rb +22 -17
  282. data/lib/permission_forge/rake_audit.rb +5 -4
  283. data/lib/permission_forge/rake_dry_run.rb +32 -0
  284. data/lib/permission_forge/rake_output.rb +189 -0
  285. data/lib/permission_forge/record_read_access.rb +51 -0
  286. data/lib/permission_forge/resource_chain.rb +14 -2
  287. data/lib/permission_forge/schema_read/ordered_types.rb +72 -0
  288. data/lib/permission_forge/schema_read.rb +335 -0
  289. data/lib/permission_forge/secret_columns.rb +49 -0
  290. data/lib/permission_forge/sensitive_columns.rb +114 -0
  291. data/lib/permission_forge/services/attribute_access_guard.rb +22 -14
  292. data/lib/permission_forge/services/bulk_permission_set_assignment.rb +16 -8
  293. data/lib/permission_forge/services/concerns/clonable.rb +9 -0
  294. data/lib/permission_forge/services/concerns/settles_cache.rb +58 -0
  295. data/lib/permission_forge/services/consent_withdrawal_handler.rb +37 -19
  296. data/lib/permission_forge/services/data_subject_erasure.rb +83 -45
  297. data/lib/permission_forge/services/expired_assignment_cleaner.rb +62 -25
  298. data/lib/permission_forge/services/extra_field_defaults.rb +1 -1
  299. data/lib/permission_forge/services/field_permission_merger.rb +44 -17
  300. data/lib/permission_forge/services/filters_response_attributes.rb +20 -5
  301. data/lib/permission_forge/services/gdpr_field_auditor/text_report.rb +33 -5
  302. data/lib/permission_forge/services/gdpr_field_auditor.rb +68 -48
  303. data/lib/permission_forge/services/host_key_guard.rb +8 -5
  304. data/lib/permission_forge/services/mask_value.rb +72 -11
  305. data/lib/permission_forge/services/permission_cache_clearer.rb +36 -15
  306. data/lib/permission_forge/services/permission_hash_builder.rb +48 -19
  307. data/lib/permission_forge/services/permission_rule_applier.rb +131 -68
  308. data/lib/permission_forge/services/permission_set_cloner.rb +32 -8
  309. data/lib/permission_forge/services/permission_set_condition_evaluator.rb +38 -36
  310. data/lib/permission_forge/services/permission_set_group_cloner.rb +2 -1
  311. data/lib/permission_forge/services/permits_attributes/result.rb +31 -0
  312. data/lib/permission_forge/services/permits_attributes.rb +48 -27
  313. data/lib/permission_forge/services/resource_field_generator/row_builder.rb +96 -0
  314. data/lib/permission_forge/services/resource_field_generator.rb +71 -42
  315. data/lib/permission_forge/services/resource_with_fields_builder.rb +2 -1
  316. data/lib/permission_forge/services/rls_operator_sql.rb +83 -24
  317. data/lib/permission_forge/services/rls_policy_generator.rb +83 -39
  318. data/lib/permission_forge/services/rls_read_access_sql.rb +131 -0
  319. data/lib/permission_forge/services/security_audit/action_check.rb +49 -5
  320. data/lib/permission_forge/services/security_audit/assignment_check.rb +2 -1
  321. data/lib/permission_forge/services/security_audit/audit_trail_check.rb +1 -3
  322. data/lib/permission_forge/services/security_audit/conditions_check.rb +92 -0
  323. data/lib/permission_forge/services/security_audit/config_check.rb +41 -18
  324. data/lib/permission_forge/services/security_audit/integrity_check.rb +71 -4
  325. data/lib/permission_forge/services/security_audit/masking_without_user.rb +66 -0
  326. data/lib/permission_forge/services/security_audit/personal_data_check.rb +7 -11
  327. data/lib/permission_forge/services/security_audit/rule_check.rb +180 -50
  328. data/lib/permission_forge/services/security_audit/text_report.rb +14 -2
  329. data/lib/permission_forge/services/security_audit/unmanaged_actions.rb +145 -0
  330. data/lib/permission_forge/services/security_audit/user_model_check.rb +4 -3
  331. data/lib/permission_forge/services/security_audit.rb +16 -4
  332. data/lib/permission_forge/services/stale_assignment_auditor.rb +8 -5
  333. data/lib/permission_forge/timestamp_fields.rb +60 -0
  334. data/lib/permission_forge/unique_conflict.rb +117 -0
  335. data/lib/permission_forge/version.rb +1 -1
  336. data/lib/permission_forge/virtual_fields/discovery.rb +2 -1
  337. data/lib/permission_forge/virtual_fields.rb +15 -5
  338. data/lib/permission_forge/warnings.rb +263 -0
  339. data/lib/permission_forge/web_request.rb +248 -0
  340. data/lib/permission_forge.rb +54 -0
  341. data/lib/tasks/permission_forge.rake +26 -6
  342. data/lib/tasks/permission_forge_audit_events.rake +8 -4
  343. data/lib/tasks/permission_forge_cache.rake +12 -12
  344. data/lib/tasks/permission_forge_docs.rake +48 -0
  345. data/lib/tasks/permission_forge_expiration.rake +11 -5
  346. data/lib/tasks/permission_forge_gdpr.rake +36 -20
  347. metadata +84 -1
@@ -84,14 +84,27 @@ module PermissionForge
84
84
  end
85
85
 
86
86
  # Without a user there is no entry to decide by:
87
- # config.masked_attributes_nil_user_strategy says whether every
88
- # attribute passes (:bypass, the default) or is hidden (:hide) — except
89
- # on the user model, which sign-in reads before anyone is signed in.
90
- # The model is the record's class, or else the one the first name of the
91
- # chain names (an anonymous subclass answers as its base).
87
+ # config.masked_attributes_nil_user_strategy says whether the attribute
88
+ # passes (:bypass, the default), is hidden (:hide), or is hidden inside
89
+ # a web request only (:hide_in_requests) — never on the user model,
90
+ # which sign-in reads before anyone is signed in
91
+ # (Configuration#masked_attributes_without_user_for). A read returned
92
+ # as stored inside a web request under :bypass is recorded
93
+ # (Audit::UnmaskedWithoutUser). The model is the record's class, or
94
+ # else the one the first name of the chain names (an anonymous
95
+ # subclass answers as its base).
92
96
  def without_user
93
- model = @model || @resource_name.to_s.safe_constantize
94
- PermissionForge.configuration.hide_masked_attributes_without_user_for?(model) ? hidden : bypass
97
+ model = @model || PermissionForge::ModelLookup.model_for(@resource_name)
98
+ case PermissionForge.configuration.masked_attributes_without_user_for(model)
99
+ when :hide then hidden
100
+ when :record then unmasked_in_request(model)
101
+ else bypass
102
+ end
103
+ end
104
+
105
+ def unmasked_in_request(model)
106
+ PermissionForge::Audit::UnmaskedWithoutUser.note(model || @resource_name, @attribute)
107
+ bypass
95
108
  end
96
109
 
97
110
  def bypass
@@ -116,13 +129,8 @@ module PermissionForge
116
129
  end
117
130
 
118
131
  def unconfigured
119
- strategy_key = PermissionForge.configuration.unconfigured_field_strategy
120
- # Unknown strategy keys fall back to :block (fail closed), never pass-through.
121
- strategy = FiltersResponseAttributes::UNCONFIGURED_FIELD_STRATEGIES.fetch(
122
- strategy_key,
123
- FiltersResponseAttributes::UNCONFIGURED_FIELD_STRATEGIES[:block]
124
- )
125
- include_field, value = strategy.call(@resource_name.to_s, @attribute, @raw_value)
132
+ # FiltersResponseAttributes.unconfigured_strategy: the one lookup (an unknown key blocks).
133
+ include_field, value = FiltersResponseAttributes.unconfigured_strategy.call(@resource_name.to_s, @attribute, @raw_value)
126
134
  notify(:unconfigured)
127
135
  [include_field, value]
128
136
  end
@@ -1,3 +1,5 @@
1
+ require_relative '../cache/report'
2
+
1
3
  module PermissionForge
2
4
  module Services
3
5
  # Assigns or revokes a permission set for multiple users in a single
@@ -10,7 +12,7 @@ module PermissionForge
10
12
  # result = BulkPermissionSetAssignment.call(
11
13
  # permission_set: ps, user_ids: [1, 2, 3], action: :assign
12
14
  # )
13
- # # => { assigned: 3, revoked: 0, invalidated: 3 }
15
+ # # => { assigned: 3, revoked: 0, invalidated: 3, cache: { confirmed: true, process_local: false, unconfirmed: 0 } }
14
16
  #
15
17
  # `invalidated` counts the users assigned or revoked whose invalidation
16
18
  # the cache store confirmed. Fewer means the store failed for the rest
@@ -18,7 +20,11 @@ module PermissionForge
18
20
  # may live until cache_ttl. A user who already held the set (assign) or
19
21
  # never did (revoke) changed nothing and is not invalidated. Inside a
20
22
  # transaction of the caller the invalidations happen at its commit, after
21
- # this returns: `invalidated` is then nil (not known).
23
+ # this returns: `invalidated` is then nil (not known). `cache` is the one
24
+ # Cache::Outcome every cache-touching operation answers (Cache::Report):
25
+ # `confirmed` (nil inside the caller's transaction), `process_local`
26
+ # (the store lives in this process only, so other processes kept their
27
+ # entries), `unconfirmed`.
22
28
  #
23
29
  # Every row is created or destroyed through its callbacks, so each person
24
30
  # gets an assignment.granted / assignment.revoked audit event, all with
@@ -35,7 +41,7 @@ module PermissionForge
35
41
  VALID_ACTIONS = ACTION_STRATEGIES.keys.freeze
36
42
 
37
43
  # Assigns or revokes a Permission Set for many users in one transaction.
38
- # @return [Hash] `{ assigned:, revoked:, invalidated: }`
44
+ # @return [Hash] `{ assigned:, revoked:, invalidated:, cache: }`
39
45
  def self.call(...) = new(...).call
40
46
 
41
47
  # @raise [ArgumentError] unless `action` is one of VALID_ACTIONS
@@ -59,17 +65,19 @@ module PermissionForge
59
65
  @changed_user_ids = []
60
66
  end
61
67
 
62
- # @return [Hash] `{ assigned:, revoked:, invalidated: }`
68
+ # @return [Hash] `{ assigned:, revoked:, invalidated:, cache: }`
63
69
  def call
64
70
  validate_action!
65
71
 
66
- deferred = PermissionForge::OpenTransaction.current
67
- failures = PermissionForge::Cache::Failure.collect do
72
+ # what the store confirmed (Cache::Report) — not what a host's override of invalidate_permissions returns;
73
+ # pending inside a caller's transaction (Report#deferred?)
74
+ report = PermissionForge::Cache::Report.measure do
68
75
  ActiveRecord::Base.transaction { send(ACTION_STRATEGIES.fetch(@action)) }
69
76
  end
77
+ outcome = report.outcome(user_ids: @changed_user_ids)
70
78
 
71
- # what the store confirmed (Cache::Failure) — not what a host's override of invalidate_permissions returns
72
- { assigned: @assigned_count, revoked: @revoked_count, invalidated: deferred ? nil : failures.confirmed(@changed_user_ids).size }
79
+ { assigned: @assigned_count, revoked: @revoked_count,
80
+ invalidated: report.deferred? ? nil : @changed_user_ids.size - outcome.unconfirmed, cache: outcome.to_h }
73
81
  end
74
82
 
75
83
  private
@@ -24,6 +24,15 @@ module PermissionForge
24
24
  def reset_on_copy(record)
25
25
  RESET_ON_COPY.select { |column| record.has_attribute?(column) }.index_with(nil)
26
26
  end
27
+
28
+ # A row whose blank name the validations would refuse on a new record
29
+ # (ResourceField, ResourceAction: `validates :name, presence`), so a
30
+ # copy of it can never be saved.
31
+ def refused_for_blank_name?(record)
32
+ return false unless record.respond_to?(:name) && record.name.to_s.strip.empty?
33
+
34
+ record.class.validators_on(:name).any? { |validator| validator.kind == :presence }
35
+ end
27
36
  end
28
37
  end
29
38
  end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Services
5
+ module Concerns
6
+ # For an operation whose summary event is written in its transaction,
7
+ # before its cache work (erasure.performed, consent.withdrawn record
8
+ # `cache_confirmed: nil`): one `cache.settled` event, on the
9
+ # operation's correlation id, says what the cache store answered once
10
+ # the work ran.
11
+ #
12
+ # Outside a transaction that is the outcome the operation just
13
+ # measured. Inside a caller's one (Cache::Report#deferred?) the work is
14
+ # only done for everybody at its commit: it runs again there, measured,
15
+ # and the event is written then, in the context of the operation
16
+ # (OpenTransaction#after_commit: never after a rollback). The work
17
+ # always runs, also with auditing off; a failure there is reported,
18
+ # never raised out of the commit, and a store failure is what the event
19
+ # records (`cache_confirmed: false`).
20
+ #
21
+ # @api private
22
+ module SettlesCache
23
+ EVENT = 'cache.settled'
24
+
25
+ private
26
+
27
+ # @param operation [String] the operation's name in the event's data ('erasure')
28
+ # @param subject [Object] the person whose cached permissions it cleared
29
+ # @param report [Cache::Report] the operation's measured report
30
+ # @yield the cache work, run again after a caller's commit
31
+ def settle_cache(operation, subject, report, &work)
32
+ transaction = PermissionForge::OpenTransaction.current if report.deferred?
33
+ return record_settled(operation, subject, report.outcome) unless transaction
34
+
35
+ context = PermissionForge::Audit::Context.current
36
+ transaction.after_commit do
37
+ PermissionForge::Audit::Context.with(**context) do
38
+ record_settled(operation, subject, PermissionForge::Cache::Report.of(&work))
39
+ end
40
+ rescue StandardError => e
41
+ PermissionForge::Cache::Failure.report(operation: :invalidate, user_id: subject.id, error: e)
42
+ end
43
+ end
44
+
45
+ # Written after the operation's transaction: like Audit::Detached, a
46
+ # failure to store it is reported (or logged), never raised — the
47
+ # operation has happened.
48
+ def record_settled(operation, subject, outcome)
49
+ PermissionForge::Audit.record(EVENT, subject_user_id: subject.id,
50
+ data: { 'operation' => operation, **outcome.event_data })
51
+ rescue StandardError => e
52
+ strategy = PermissionForge.configuration.audit_hook_failure_strategy
53
+ PermissionForge::Audit::Failure.handle(e, strategy: strategy == :raise ? :report : strategy, operation: :store, event: EVENT)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -1,3 +1,5 @@
1
+ require_relative '../cache/report'
2
+
1
3
  module PermissionForge
2
4
  module Services
3
5
  # Handles consent withdrawal by removing a user from all permission sets
@@ -9,11 +11,20 @@ module PermissionForge
9
11
  # user: user,
10
12
  # consent_flag: 'marketing_consent'
11
13
  # )
12
- # # => { revoked_sets: 2, invalidated: true }
14
+ # # => { revoked_sets: 2, invalidated: true, affected_sets: ["Marketing Access", "Newsletter"],
15
+ # # affected_assignment_count: 2, dry_run: false,
16
+ # # cache: { confirmed: true, process_local: false, unconfirmed: 0 } }
13
17
  #
14
18
  # `invalidated` is false when nothing was revoked, and when the cache
15
19
  # store did not confirm the invalidation (reported through
16
- # `config.on_cache_failure`).
20
+ # `config.on_cache_failure`). `cache` is the one Cache::Outcome every
21
+ # cache-touching operation answers (Cache::Report): `confirmed` (true
22
+ # also when nothing had to be invalidated), `process_local` (the store
23
+ # lives in this process only: other processes kept their entries),
24
+ # `unconfirmed`. A dry run and a real run answer the same keys:
25
+ # `affected_sets` (the names of the sets gated on the flag),
26
+ # `affected_assignment_count` (this person's assignments to them),
27
+ # `dry_run`; a dry run's `revoked_sets` is 0 and its `invalidated` false.
17
28
  #
18
29
  # @example Dry run (inspect without revoking)
19
30
  # result = ConsentWithdrawalHandler.call(
@@ -21,7 +32,8 @@ module PermissionForge
21
32
  # consent_flag: 'marketing_consent',
22
33
  # dry_run: true
23
34
  # )
24
- # # => { revoked_sets: 0, affected_sets: ["Marketing Access"], dry_run: true }
35
+ # # => { revoked_sets: 0, invalidated: false, affected_sets: ["Marketing Access"],
36
+ # # affected_assignment_count: 1, dry_run: true, cache: { confirmed: nil, work: false, ... } }
25
37
  #
26
38
  # The audit log: assignments are destroyed through their callbacks (one
27
39
  # `assignment.revoked` each), then ONE `consent.withdrawn` event names the
@@ -32,8 +44,10 @@ module PermissionForge
32
44
  # The revocations and `consent.withdrawn` happen in ONE transaction, like
33
45
  # DataSubjectErasure: if the summary cannot be written under
34
46
  # 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).
47
+ # cache invalidation runs after the commit, so the summary records the
48
+ # outcome as pending (`cache_confirmed: nil`, the store's scope in
49
+ # `process_local`); the result's `invalidated` and `cache` say what the
50
+ # store confirmed.
37
51
  #
38
52
  # Tenants: with a tenant ambient, the person's assignments in that
39
53
  # tenant. With none (a platform task) in every tenant — also under
@@ -46,6 +60,7 @@ module PermissionForge
46
60
  class ConsentWithdrawalHandler
47
61
  include Concerns::AuditedOperation
48
62
  include Concerns::AcrossTenants
63
+ include Concerns::SettlesCache
49
64
 
50
65
  # Removes a user from every Permission Set gated on the consent flag.
51
66
  # @return [Hash] see {#call}
@@ -59,27 +74,31 @@ module PermissionForge
59
74
  @reason = reason
60
75
  end
61
76
 
62
- # @return [Hash] `{ revoked_sets:, invalidated: }` (plus `affected_sets:` and `dry_run:` for a dry run)
77
+ # @return [Hash] `{ revoked_sets:, invalidated:, affected_sets:, affected_assignment_count:, dry_run:, cache: }`
63
78
  def call
64
79
  validate!
65
80
 
66
81
  across_tenants do
67
82
  affected_sets = consent_gated_sets
68
83
  affected_assignments = user_assignments_for(affected_sets)
69
- next dry_run_result(affected_sets, affected_assignments) if @dry_run
84
+ found = found(affected_sets, affected_assignments)
85
+ next found.merge(revoked_sets: 0, invalidated: false, cache: PermissionForge::Cache::Report.none.to_h) if @dry_run
70
86
 
71
- audited(actor: @actor, reason: @reason) { withdraw(affected_assignments) }
87
+ audited(actor: @actor, reason: @reason) { withdraw(affected_assignments).merge(found) }
72
88
  end
73
89
  end
74
90
 
75
91
  private
76
92
 
77
93
  def withdraw(affected_assignments)
94
+ @report = PermissionForge::Cache::Report.new
78
95
  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 }
96
+ # what the store confirmed — not what a host's override of the method returns;
97
+ # pending inside a caller's transaction (Report#deferred?)
98
+ outcome = @report.measure { @user.invalidate_permissions if revoked_count.positive? }.outcome
99
+ settle_cache('consent_withdrawal', @user, @report) { @user.invalidate_permissions } if revoked_count.positive?
81
100
 
82
- { revoked_sets: revoked_count, invalidated: invalidated }
101
+ { revoked_sets: revoked_count, invalidated: revoked_count.positive? && outcome.confirmed, cache: outcome.to_h }
83
102
  end
84
103
 
85
104
  def revoke_and_record(affected_assignments)
@@ -97,6 +116,7 @@ module PermissionForge
97
116
  end
98
117
  end
99
118
 
119
+ # The cache outcome is pending here: the invalidation follows the commit.
100
120
  def summary_data(sets, revoked_count)
101
121
  {
102
122
  'operation' => 'consent_withdrawal',
@@ -105,7 +125,8 @@ module PermissionForge
105
125
  'set_uuids' => sets.map(&:uuid).uniq,
106
126
  'set_names' => sets.map(&:name).uniq,
107
127
  'revoked' => revoked_count,
108
- 'dry_run' => false
128
+ 'dry_run' => false,
129
+ **@report.pending.event_data
109
130
  }
110
131
  end
111
132
 
@@ -122,13 +143,10 @@ module PermissionForge
122
143
  PermissionForge::PermissionSetUser.where(user: @user, permission_set: sets)
123
144
  end
124
145
 
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
- }
146
+ # What the run found before it removed anything: the same keys for a
147
+ # dry run and a real one.
148
+ def found(sets, assignments)
149
+ { affected_sets: sets.pluck(:name), affected_assignment_count: assignments.count, dry_run: @dry_run ? true : false }
132
150
  end
133
151
  end
134
152
  end
@@ -1,3 +1,5 @@
1
+ require_relative '../cache/report'
2
+
1
3
  module PermissionForge
2
4
  module Services
3
5
  # Supports a GDPR Art. 17 (right to erasure) request for a host user.
@@ -26,7 +28,9 @@ module PermissionForge
26
28
  #
27
29
  # @example Full erasure
28
30
  # result = DataSubjectErasure.call(user: user)
29
- # # => { assignments_removed: 3, group_assignments_removed: 1, cache_cleared: true, callback_invoked: true }
31
+ # # => { assignments_removed: 3, group_assignments_removed: 1, permission_sets: ["Sales"], groups: [],
32
+ # # actor_labels_cleared: 2, record_labels_cleared: 6, events_scrubbed: 8, cache_cleared: true,
33
+ # # callback_invoked: true, cache: { confirmed: true, process_local: false, unconfirmed: 0 }, ... }
30
34
  #
31
35
  # `cache_cleared` is false when the cache store did not confirm the
32
36
  # removal (it is unreachable, say): the assignments are gone, but the
@@ -37,9 +41,22 @@ module PermissionForge
37
41
  # transaction has committed, and a failure then is reported through
38
42
  # `config.on_cache_failure` alone.
39
43
  #
44
+ # `cache` is the one Cache::Outcome every cache-touching operation
45
+ # answers (Cache::Report): `confirmed` (as `cache_cleared`),
46
+ # `process_local`, `unconfirmed`. `cache_cleared` speaks for the store
47
+ # this process reaches. When that store lives in this process only (an
48
+ # in-memory store, Cache::Base#process_local? — `cache_backend = :memory`,
49
+ # or `Rails.cache` a `:memory_store`), a run from a rake task or a console
50
+ # cleared nothing in the web and job processes: they keep the person's
51
+ # cached permissions until they restart or the entries expire
52
+ # (cache_ttl). `erasure.performed` then says `process_local: true`; its
53
+ # `cache_confirmed` is nil, because the event is written in the
54
+ # erasure's transaction and the cache work follows it.
55
+ #
40
56
  # @example Dry run (inspect without removing)
41
57
  # result = DataSubjectErasure.call(user: user, dry_run: true)
42
- # # => { assignments_count: 3, group_assignments_count: 1, permission_sets: [...], dry_run: true }
58
+ # # => { assignments_count: 3, group_assignments_count: 1, permission_sets: [...],
59
+ # # actor_labels_to_clear: 2, record_labels_to_clear: 4, dry_run: true }
43
60
  #
44
61
  # The audit log (docs/audit-log.md): the rows are destroyed through their
45
62
  # callbacks, so each assignment writes its own `assignment.revoked`
@@ -77,6 +94,10 @@ module PermissionForge
77
94
  class DataSubjectErasure
78
95
  include Concerns::AuditedOperation
79
96
  include Concerns::AcrossTenants
97
+ include Concerns::SettlesCache
98
+
99
+ # The name labels erasure clears, by the count they are reported under.
100
+ LABELS = { actor_labels: :actor_label, record_labels: :record_label }.freeze
80
101
 
81
102
  # Erases a user's permission data (GDPR Art. 17).
82
103
  # @return [Hash] see {#call}
@@ -104,22 +125,31 @@ module PermissionForge
104
125
  private
105
126
 
106
127
  def erase
128
+ @report = PermissionForge::Cache::Report.new
107
129
  removed = ActiveRecord::Base.transaction { remove_and_record }
108
- cache_cleared = clear_cache
130
+ outcome = clear_cache
131
+ settle_cache('erasure', @user, @report) { remove_user_cache }
109
132
 
110
- removed.merge(cache_cleared: cache_cleared, callback_invoked: run_callback)
133
+ removed.merge(cache_cleared: outcome.confirmed, callback_invoked: run_callback, cache: outcome.to_h)
111
134
  end
112
135
 
113
136
  def remove_and_record
114
137
  sets = permission_set_assignments.includes(:permission_set).filter_map(&:permission_set)
115
138
  groups = group_assignments.includes(:permission_set_group).filter_map(&:permission_set_group)
116
139
 
117
- counts = { assignments_removed: remove_permission_set_assignments,
118
- group_assignments_removed: remove_group_assignments }
140
+ counts = { assignments_removed: remove_all(permission_set_assignments),
141
+ group_assignments_removed: remove_all(group_assignments) }
119
142
  counts.merge!(remaining_elsewhere)
120
143
  scrubbed = scrub_labels
121
144
  record_summary(sets, groups, counts, scrubbed)
122
- counts
145
+ counts.merge(result_details(sets, groups, scrubbed))
146
+ end
147
+
148
+ # The names removed and the labels cleared, as the result reports them.
149
+ def result_details(sets, groups, scrubbed)
150
+ { permission_sets: sets.map(&:name).uniq, groups: groups.map(&:name).uniq,
151
+ actor_labels_cleared: scrubbed[:actor_labels], record_labels_cleared: scrubbed[:record_labels],
152
+ events_scrubbed: scrubbed.values.sum }
123
153
  end
124
154
 
125
155
  # What the person still has in other tenants (nothing is removed there).
@@ -149,14 +179,24 @@ module PermissionForge
149
179
  # basic auth login that shares the id is somebody else), and recorded
150
180
  # in `erasure.performed`.
151
181
  def scrub_labels
152
- events = scrubbed_events
153
- id = @user.id.to_s
154
182
  {
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)
183
+ actor_labels: labelled(:actor_labels).update_all(actor_label: nil),
184
+ record_labels: labelled(:record_labels).update_all(record_label: nil)
157
185
  }
158
186
  end
159
187
 
188
+ # The events whose label names the person: as the actor (an actor of
189
+ # type 'user', in actor_label) or as the subject (in record_label).
190
+ def labelled(kind)
191
+ id = @user.id.to_s
192
+ events = if kind == :actor_labels
193
+ scrubbed_events.where(actor_type: 'user', actor_id: id)
194
+ else
195
+ scrubbed_events.where(subject_user_id: id)
196
+ end
197
+ events.where.not(LABELS.fetch(kind) => nil)
198
+ end
199
+
160
200
  # With a tenant current (a tenant admin), that tenant's events only: a
161
201
  # tenant cannot reach another tenant's log. With none (the rake task, a
162
202
  # platform operator), every tenant's, and the events without a tenant.
@@ -172,12 +212,15 @@ module PermissionForge
172
212
  data: summary_data(sets, groups, counts, scrubbed))
173
213
  end
174
214
 
215
+ # The cache outcome is pending here: the invalidation follows this
216
+ # transaction (@report.pending: the store's scope, nothing confirmed).
175
217
  def summary_data(sets, groups, counts, scrubbed)
176
218
  cleared = { 'events_scrubbed' => scrubbed.values.sum, 'actor_labels_cleared' => scrubbed[:actor_labels],
177
219
  'record_labels_cleared' => scrubbed[:record_labels] }
178
220
  names(sets, 'set').merge(names(groups, 'group'), counts.transform_keys(&:to_s), cleared,
179
221
  'operation' => 'erasure', 'subject_user_id' => @user.id.to_s,
180
- 'callback_invoked' => !@on_erasure.nil?, 'dry_run' => false)
222
+ 'callback_invoked' => !@on_erasure.nil?, 'dry_run' => false,
223
+ **@report.pending.event_data)
181
224
  end
182
225
 
183
226
  def names(records, kind)
@@ -196,42 +239,41 @@ module PermissionForge
196
239
  PermissionForge::PermissionSetGroupUser.where(user: @user)
197
240
  end
198
241
 
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
242
+ # Destroys the rows through their callbacks (one audit event each).
243
+ #
244
+ # @return [Integer] how many there were
245
+ def remove_all(records)
208
246
  count = records.count
209
247
  records.destroy_all
210
248
  count
211
249
  end
212
250
 
213
- # Asks the gem's own failure bookkeeping, not the value of
214
- # invalidate_permissions: a host app overrides that method.
251
+ # Measured by Cache::Report (the gem's own failure bookkeeping), not
252
+ # the value of invalidate_permissions: a host app overrides that method.
215
253
  #
216
254
  # An invalidation leaves the per-context variants of a user with
217
255
  # conditional sets to their TTL (unreachable). They hold the profile,
218
256
  # 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.
257
+ # caller's transaction once it has committed (SettlesCache runs the
258
+ # work again there, measured, and records cache.settled), after the
259
+ # assignments' own invalidations: until then another request may still
260
+ # cache one from the rows as they were.
222
261
  #
223
- # @return [Boolean, nil] whether the store confirmed the removal; nil
224
- # inside an open transaction, where that is only known at its commit
262
+ # @return [PermissionForge::Cache::Outcome] confirmed nil inside an
263
+ # open transaction, where that is only known at its commit
225
264
  def clear_cache
226
- transaction = PermissionForge::OpenTransaction.current
227
- cleared = PermissionForge::Cache::Failure.none? do
265
+ @report.measure do
228
266
  invalidate_user
229
- PermissionForge::Cache::VariantRemoval.call(**variant_removal) unless transaction
267
+ PermissionForge::Cache::VariantRemoval.call(**variant_removal) unless @report.deferred?
230
268
  end
231
- return cleared unless transaction
269
+ @report.outcome
270
+ end
232
271
 
233
- remove_variants_after_commit(transaction)
234
- nil
272
+ # The person's cache work after a caller's commit: the invalidation and
273
+ # every per-context variant.
274
+ def remove_user_cache
275
+ invalidate_user
276
+ PermissionForge::Cache::VariantRemoval.call(**variant_removal)
235
277
  end
236
278
 
237
279
  # The arguments of the person's Cache::VariantRemoval: the keys the
@@ -243,17 +285,6 @@ module PermissionForge
243
285
  { user_ids: [@user.id], indexed_only: false, **named }
244
286
  end
245
287
 
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
288
  def invalidate_user
258
289
  # invalidate_permissions (not a raw cache delete) so the ambient and
259
290
  # tenant-agnostic keys are both removed AND the process-wide memo
@@ -293,11 +324,18 @@ module PermissionForge
293
324
  Rails.error.report(e, handled: true, source: 'permission_forge.data_subject_erasure')
294
325
  end
295
326
 
327
+ # The label counts (actor_labels_to_clear, record_labels_to_clear) are of
328
+ # the events there now — none on an app whose audit table is not there
329
+ # yet; the events the erasure itself writes (assignment.revoked, ...) are
330
+ # cleared as well.
296
331
  def dry_run_result
332
+ table = PermissionForge::DatabaseReach.table_exists?(PermissionForge::AuditEvent)
297
333
  {
298
334
  assignments_count: permission_set_assignments.count,
299
335
  group_assignments_count: group_assignments.count,
300
336
  permission_sets: permission_set_assignments.includes(:permission_set).filter_map { |psu| psu.permission_set&.name },
337
+ **LABELS.keys.to_h { |kind| [:"#{kind}_to_clear", table ? labelled(kind).count : 0] },
338
+ cache: PermissionForge::Cache::Report.none.to_h,
301
339
  dry_run: true
302
340
  }
303
341
  end