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
@@ -422,6 +422,42 @@
422
422
  }
423
423
  });
424
424
 
425
+ /* ------------------------------------------- rule builder preview */
426
+ // The preview is the sentence the saved rule shows, rendered by the server
427
+ // (the preview action, SentencesHelper): as the value, "the current user"
428
+ // or "or empty" change, the panel's content is fetched again. Field and
429
+ // operator changes re-render the whole builder (refresh above).
430
+ // Only the latest request may fill the panel: a newer input aborts the one
431
+ // before, so a late answer never overwrites a newer sentence. An answer that
432
+ // is no fragment — a redirect (a lapsed session's login page), a whole
433
+ // document — leaves the panel as it is: the builder's refresh and save,
434
+ // the no-JS path, still show the sentence.
435
+ let previewRequest = null;
436
+ function previewRule(form) {
437
+ const panel = form && $('[data-pf-rule-preview]', form);
438
+ if (!panel) return;
439
+ const data = new FormData(form); data.delete('authenticity_token'); data.delete('_method');
440
+ const base = panel.getAttribute('data-pf-rule-preview');
441
+ const url = base + (base.indexOf('?') < 0 ? '?' : '&') + new URLSearchParams(data).toString();
442
+ if (previewRequest) previewRequest.abort();
443
+ const request = new AbortController();
444
+ previewRequest = request;
445
+ fetch(url, { headers: { [FRAGMENT_HEADER]: '1' }, credentials: 'same-origin', signal: request.signal })
446
+ .then(function (res) { return res.ok && !res.redirected ? res.text() : Promise.reject(res.status); })
447
+ .then(function (html) {
448
+ if (request !== previewRequest || /<html[\s>]/i.test(html)) return;
449
+ setContent(panel, html);
450
+ })
451
+ .catch(function () {})
452
+ .then(function () { if (previewRequest === request) previewRequest = null; });
453
+ }
454
+ const previewHandler = debounce(previewRule, 250);
455
+ const PREVIEW_INPUTS = '[name="rule[value]"], [name="rule[values_text]"], [name="rule[values][]"], [name="rule[value_from]"], ' +
456
+ '[name="rule[value_to]"], [name="rule[current_user]"], [name="rule[include_nulls]"]';
457
+ ['input', 'change'].forEach(function (type) {
458
+ on(type, PREVIEW_INPUTS, function (e, el) { previewHandler(el.form); });
459
+ });
460
+
425
461
  /* ------------------------------------------- typed confirmation */
426
462
  on('input', 'input[data-pf-confirm-match]', function (e, input) {
427
463
  const btn = input.form && input.form.querySelector('[data-pf-confirm-target]');
@@ -4,10 +4,15 @@ module PermissionForge
4
4
  include PermissionForge::UiHelper
5
5
  include PermissionForge::BadgesHelper
6
6
  include PermissionForge::PeopleHelper
7
+ include PermissionForge::LabelsHelper
7
8
  include PermissionForge::SentencesHelper
9
+ include PermissionForge::ConditionsHelper
10
+ include PermissionForge::RuleProblemsHelper
8
11
  include PermissionForge::AuditSentencesHelper
9
12
  include PermissionForge::ChangesHelper
10
13
  include PermissionForge::AuditHelper
14
+ include PermissionForge::AuditRecordsHelper
15
+ include PermissionForge::AuditAttributionHelper
11
16
  include PermissionForge::SettingsHelper
12
17
 
13
18
  # Sidebar link; the current section gets aria-current="page".
@@ -54,10 +59,11 @@ module PermissionForge
54
59
  Rails.application.class.module_parent_name.to_s.titleize.presence
55
60
  end
56
61
 
62
+ # @deprecated Use {UiHelper#pf_relative}. Read Rails' datetime.distance_in_words keys
63
+ # (English without rails-i18n); now the same as pf_relative, removed in 0.16.
57
64
  def pf_time_ago(time)
58
- return t('permission_forge.shared.time_nil') if time.nil?
59
-
60
- t('permission_forge.shared.time_ago', time: time_ago_in_words(time))
65
+ PermissionForge::Deprecation.warn('pf_time_ago is deprecated; use pf_relative', key: :pf_time_ago)
66
+ pf_relative(time)
61
67
  end
62
68
 
63
69
  def pf_available_locales
@@ -86,82 +92,26 @@ module PermissionForge
86
92
  "#{request.path}?locale=#{CGI.escape(loc)}"
87
93
  end
88
94
 
89
- # Display label for a user in the assignment dropdown.
90
- #
91
- # Honors `config.user_label_formatter` if configured. Otherwise tries
92
- # `:name`, `:full_name`, `:display_name`, then `:email`, then composes
93
- # `first_name`/`last_name`, and finally falls back to `User #<id>`.
94
- # The id is appended in `#<id>` form to keep labels unique.
95
+ # Display label for a user in the assignment dropdown: the person's
96
+ # label (Admin::PersonLabel — `config.user_label_formatter`, else name
97
+ # then email, else "User"), with the email in parentheses when the
98
+ # label is a name and no formatter is configured. The id is appended in
99
+ # `#<id>` form to keep labels unique.
95
100
  def pf_user_option_label(user)
96
- formatter = PermissionForge.configuration.user_label_formatter
97
- base = formatter ? formatter.call(user).to_s.strip : pf_default_user_label(user)
98
- base = 'User' if base.blank?
101
+ label = pf_person(user)
102
+ return "#{t('permission_forge.shared.user')} ##{user.id}" if label.fallback?
103
+
104
+ email = label.secondary unless PermissionForge.configuration.user_label_formatter
105
+ base = email ? "#{label.name} (#{email})" : label.name
99
106
  "#{base} ##{user.id}"
100
107
  end
101
108
 
102
109
  private
103
110
 
104
- def pf_default_user_label(user)
105
- name = pf_first_present(user, :name, :full_name, :display_name) || pf_compose_name(user)
106
- email = pf_raw_user_attribute(user, :email).to_s.strip.presence
107
- pf_format_name_and_email(name, email)
108
- end
109
-
110
- def pf_compose_name(user)
111
- [pf_raw_user_attribute(user, :first_name), pf_raw_user_attribute(user, :last_name)]
112
- .compact.map { |part| part.to_s.strip }.reject(&:empty?).join(' ').presence
113
- end
114
-
115
- def pf_format_name_and_email(name, email)
116
- return "#{name} (#{email})" if name && email
117
- return name if name
118
- return email if email
119
-
120
- t('permission_forge.shared.user')
121
- end
122
-
123
- def pf_first_present(user, *methods)
124
- methods.each do |m|
125
- value = pf_raw_user_attribute(user, m).to_s.strip
126
- return value unless value.empty?
127
- end
128
- nil
129
- end
130
-
131
- # Reads a user attribute for the admin assignment dropdown, bypassing
132
- # MaskedAttributes. The dropdown is permission-management infrastructure —
133
- # admins need to see user labels to assign permissions — so it must not be
134
- # subject to the same field masking as ordinary reads.
135
- #
136
- # Real columns are read via `read_attribute_before_type_cast` FIRST: the
137
- # public getter on a masked model routes every call through
138
- # AttributeAccessGuard (a per-attribute permission lookup) and can hand
139
- # back a masked or hidden sentinel — wrong for these labels and, across a
140
- # large dropdown, the difference between milliseconds and tens of seconds.
141
- # Virtual attributes (host-app methods like a composed `display_name`)
142
- # still resolve via the public method, as does a column whose raw value is
143
- # blank. Consequence: a host method overriding a *populated* column is not
144
- # consulted here — `config.user_label_formatter` is the escape hatch for
145
- # custom label logic.
111
+ # A user attribute for the admin's labels, read raw-column-first past
112
+ # MaskedAttributes (Admin::PersonLabel.raw_value: the one definition).
146
113
  def pf_raw_user_attribute(user, attr)
147
- attr_name = attr.to_s
148
- if pf_readable_column?(user, attr_name)
149
- stored = user.read_attribute_before_type_cast(attr_name)
150
- return stored if stored.to_s.strip.present?
151
- end
152
-
153
- user.try(attr)
154
- end
155
-
156
- # True when `attr_name` is a real, loaded column on an AR-like user —
157
- # i.e., safe to read raw. Guards against partial SELECTs from a custom
158
- # user_list_resolver, where an unloaded column must not be touched.
159
- def pf_readable_column?(user, attr_name)
160
- klass = user.class
161
- klass.respond_to?(:column_names) &&
162
- klass.column_names.include?(attr_name) &&
163
- user.respond_to?(:read_attribute_before_type_cast) &&
164
- (!user.respond_to?(:has_attribute?) || user.has_attribute?(attr_name))
114
+ Admin::PersonLabel.raw_value(user, attr)
165
115
  end
166
116
  end
167
117
  end
@@ -0,0 +1,77 @@
1
+ module PermissionForge
2
+ # Who created and who last edited a permission record, as the audit log
3
+ # names them: the one definition the footers, the Assignments tab,
4
+ # "Assigned by" and the audit tabs (and their CSV) use — the record's
5
+ # creation event (Admin::LastChanges.creations) or latest change event
6
+ # (Admin::LastChanges#last_edit), else the created_by / updated_by columns.
7
+ #
8
+ # @api private
9
+ module AuditAttributionHelper
10
+ # "Who · when" of an entry's creation: the creation event's actor when
11
+ # there is one (as the audit log names them), else created_by.
12
+ def pf_audit_created(entry)
13
+ t('permission_forge.ui.audit.who_when', who: pf_audit_entry_creator(entry), when: pf_date(entry.created_at))
14
+ end
15
+
16
+ # The creator of an Admin::AuditTrail entry (its creation_event is preloaded).
17
+ def pf_audit_entry_creator(entry)
18
+ event = entry.creation_event
19
+ event ? pf_audit_actor_name(event) : pf_person_name(entry.created_by)
20
+ end
21
+
22
+ # Who created a permission record, as the audit log names them — the one
23
+ # definition wherever the admin shows it (footers, assignment rows, the
24
+ # audit tabs): the actor of the record's creation event when the log has
25
+ # one (an HTTP Basic login or an API client stamps no created_by), else
26
+ # created_by, else "System". `creations` is a pf_audit_creations lookup
27
+ # over the page's records; without one that covers the record, the
28
+ # page's preloaded lookup or a lookup of its own is used.
29
+ def pf_audit_creator(record, creations = nil)
30
+ event = pf_audit_creation_event(record, creations)
31
+ event ? pf_audit_actor_name(event) : pf_person_name(record.try(:created_by))
32
+ end
33
+
34
+ # The record's creation event, or nil (see pf_audit_creator for `creations`).
35
+ def pf_audit_creation_event(record, creations = nil)
36
+ lookup = [creations, @pf_creations].find { |candidate| candidate&.covers?(record) }
37
+ (lookup || pf_audit_creations([record]))[record]
38
+ end
39
+
40
+ # The creation events of `records` in one lookup (two queries per type).
41
+ def pf_audit_creations(records)
42
+ PermissionForge::Admin::LastChanges.creations(records)
43
+ end
44
+
45
+ # "Who · when" of an entry's last edit: the latest change event's actor
46
+ # and time when there is one, else updated_by / updated_at.
47
+ def pf_audit_edited(entry)
48
+ return pf_audit_who_when(entry.updated_by, entry.updated_at, relative: true) unless entry.last_event
49
+
50
+ t('permission_forge.ui.audit.who_when', who: pf_audit_actor_name(entry.last_event), when: pf_relative(entry.updated_at))
51
+ end
52
+
53
+ # The record's last edit as an event (who really made it), or nil when
54
+ # the audit log has none for it or the record was edited after it.
55
+ # Uses the page's preloaded lookup when it covers the record.
56
+ def pf_audit_last_change(record)
57
+ lookup = @pf_last_changes if @pf_last_changes&.covers?(record)
58
+ (lookup || PermissionForge::Admin::LastChanges.new([record])).last_edit(record)
59
+ end
60
+
61
+ # One lookup of the edits and one of the creations (pf_audit_creator)
62
+ # for every footer the page shows. Nothing while the audit trail is off.
63
+ def pf_audit_preload_last_changes(records)
64
+ return unless PermissionForge.configuration.audit_trail_enabled
65
+
66
+ @pf_last_changes = PermissionForge::Admin::LastChanges.new(records)
67
+ @pf_creations = pf_audit_creations(records)
68
+ nil
69
+ end
70
+
71
+ # [created by, last edited by] names for the audit-trail CSV.
72
+ def pf_audit_people(entry)
73
+ edited_by = entry.last_event ? pf_audit_actor_name(entry.last_event) : pf_person_name(entry.updated_by)
74
+ [pf_audit_entry_creator(entry), edited_by]
75
+ end
76
+ end
77
+ end
@@ -1,139 +1,65 @@
1
1
  module PermissionForge
2
- # Labels and links for audit rows (Person → Audit, Permission Set → Audit,
3
- # CSV) and for audit events (the audit log, the timelines, an event).
2
+ # Labels for audit events (the audit log, the timelines, an event); the
3
+ # rows of the Audit tabs' record lists are AuditRecordsHelper's.
4
4
  #
5
5
  # @api private
6
6
  module AuditHelper
7
7
  # Every category has a glyph and a word (never colour alone).
8
8
  CATEGORY_GLYPHS = { 'change' => '✎', 'admin' => '⚑', 'system' => '⚙', 'access' => '◉' }.freeze
9
9
  CATEGORY_VARIANTS = { 'change' => :changed, 'admin' => :warn, 'system' => :default, 'access' => :mask }.freeze
10
- # The message Audit::EventQuery gives a filter that needs a value (Rails' own).
11
- QUERY_BLANK_MESSAGE = 'errors.messages.blank'.freeze
12
- # [record label, where] per audited type; evaluated in the view context.
13
- AUDIT_LABELS = {
14
- 'permission_set' => ->(record) { [record.name, nil] },
15
- 'group' => ->(record) { [record.name, nil] },
16
- 'resource' => ->(record) { [pf_resource_label(record), record.permission_set&.name] },
17
- 'field' => ->(record) { [record.label.presence || record.name, pf_audit_resource_where(record.resource)] },
18
- 'action' => ->(record) { [record.label.presence || record.name, pf_audit_resource_where(record.resource)] },
19
- 'rule' => ->(record) { [record.name, pf_audit_resource_where(record.resource)] },
20
- 'assignment' => ->(record) { [pf_person_name(record.user), record.permission_set&.name] },
21
- 'group_membership' => ->(record) { [pf_person_name(record.user), record.permission_set_group&.name] }
22
- }.freeze
23
-
24
- # Link target per audited type; evaluated in the view context.
25
- AUDIT_PATHS = {
26
- 'permission_set' => ->(record) { permission_forge.permission_set_path(record) },
27
- 'resource' => ->(record) { permission_forge.permission_set_resource_path(record.permission_set_id, record.name) },
28
- 'field' => ->(record) { pf_audit_nested_path(record.resource, :field, record.name) },
29
- 'action' => ->(record) { pf_audit_nested_path(record.resource, :action, record.id) },
30
- 'rule' => ->(record) { pf_audit_nested_path(record.resource, :rule, record.id) },
31
- 'assignment' => ->(record) { permission_forge.person_path(record.user_id) },
32
- 'group' => ->(record) { permission_forge.group_path(record) },
33
- 'group_membership' => ->(record) { permission_forge.group_path(record.permission_set_group_id) }
34
- }.freeze
35
-
36
- def pf_audit_type_label(type)
37
- t("permission_forge.ui.audit.types.#{type}")
38
- end
39
-
40
- # [type label, record label, where] — also used for CSV export.
41
- def pf_audit_describe(entry)
42
- record = entry.record
43
- label, where = pf_audit_label_and_where(entry.type, record)
44
- [pf_audit_type_label(entry.type), label, where]
45
- end
46
-
47
- def pf_audit_label_and_where(type, record)
48
- pf_audit_lookup(AUDIT_LABELS, type, record)
49
- end
50
-
51
- def pf_audit_resource_where(resource)
52
- return nil unless resource
53
-
54
- "#{resource.permission_set&.name} › #{pf_resource_label(resource)}"
55
- end
56
-
57
- def pf_audit_path(entry)
58
- pf_audit_lookup(AUDIT_PATHS, entry.type, entry.record)
59
- end
60
-
61
- # Runs the registry entry for `type` in the view context; nil for unknown types.
62
- def pf_audit_lookup(registry, type, record)
63
- handler = registry[type]
64
- instance_exec(record, &handler) if handler
65
- end
66
-
67
- def pf_audit_nested_path(resource, kind, key)
68
- return nil unless resource
69
-
70
- case kind
71
- when :field then permission_forge.permission_set_resource_field_path(resource.permission_set_id, resource.name, key)
72
- when :action then permission_forge.permission_set_resource_action_path(resource.permission_set_id, resource.name, key)
73
- when :rule then permission_forge.edit_permission_set_resource_rule_path(resource.permission_set_id, resource.name, key)
74
- end
75
- end
76
-
10
+ # The message Audit::EventQuery gives a filter that needs a value.
11
+ QUERY_BLANK_MESSAGE = 'permission_forge.validations.blank'.freeze
77
12
  def pf_audit_who_when(person, time, relative: false)
78
13
  t('permission_forge.ui.audit.who_when', who: pf_person_name(person),
79
14
  when: relative ? pf_relative(time) : pf_date(time))
80
15
  end
81
16
 
82
- # "Who · when" of an entry's last edit: the latest change event's actor
83
- # and time when there is one, else updated_by / updated_at.
84
- def pf_audit_edited(entry)
85
- return pf_audit_who_when(entry.updated_by, entry.updated_at, relative: true) unless entry.last_event
86
-
87
- t('permission_forge.ui.audit.who_when', who: pf_audit_actor_name(entry.last_event), when: pf_relative(entry.updated_at))
88
- end
17
+ # --- audit events ---------------------------------------------------------
89
18
 
90
- # The record's last edit as an event (who really made it), or nil when
91
- # the audit log has none for it or the record was edited after it.
92
- # Uses the page's preloaded lookup when it covers the record.
93
- def pf_audit_last_change(record)
94
- lookup = @pf_last_changes if @pf_last_changes&.covers?(record)
95
- (lookup || PermissionForge::Admin::LastChanges.new([record])).last_edit(record)
19
+ # Who acted, as recorded: the name snapshot, with what kind of actor it
20
+ # was when that is not a person ("ops (password prompt)", "API client").
21
+ # Admin::ActorLabel, the one definition.
22
+ def pf_audit_actor_name(event)
23
+ PermissionForge::Admin::ActorLabel.for(event)
96
24
  end
97
25
 
98
- # One lookup for every record whose footer the page shows (two queries
99
- # per record type instead of two per footer). Nothing while the audit
100
- # trail is off: the footers then say so and ask nothing.
101
- def pf_audit_preload_last_changes(records)
102
- return unless PermissionForge.configuration.audit_trail_enabled
26
+ # Why, as recorded on an event (or an assignment): a reason the gem
27
+ # wrote itself is a code (Audit::ReasonCodes: `expired`) rendered in the
28
+ # page's language; any other reason as it was typed. nil when there is
29
+ # none. The CSV export and the API keep the stored value.
30
+ def pf_audit_reason(record)
31
+ reason = record.try(:reason).to_s.strip
32
+ return if reason.empty?
33
+ return reason unless PermissionForge::Audit::ReasonCodes.code?(reason)
103
34
 
104
- @pf_last_changes = PermissionForge::Admin::LastChanges.new(records)
105
- nil
35
+ t("permission_forge.audit_log.reason_codes.#{reason}")
106
36
  end
107
37
 
108
- # [created by, last edited by] names for the audit-trail CSV.
109
- def pf_audit_people(entry)
110
- edited_by = entry.last_event ? pf_audit_actor_name(entry.last_event) : pf_person_name(entry.updated_by)
111
- [pf_person_name(entry.created_by), edited_by]
38
+ # An event that named no actor (code outside a request with no
39
+ # PermissionForge.with_audit_actor, or an actor resolver that failed):
40
+ # pf_audit_actor_name reads it as the actor type "Unknown".
41
+ def pf_audit_unknown_actor?(event)
42
+ PermissionForge::Admin::ActorLabel.unknown?(event)
112
43
  end
113
44
 
114
- # --- audit events ---------------------------------------------------------
115
-
116
- # Who acted, as recorded: the name snapshot, with what kind of actor it
117
- # was when that is not a person ("ops (password prompt)", "API client").
118
- def pf_audit_actor_name(event)
119
- type = event.actor_type.to_s
120
- label = event.actor_label.presence
121
- return t("permission_forge.audit_log.actor_named.#{type}", name: label, default: label) if label
122
- return t('permission_forge.audit_log.person_id', id: event.actor_id) if type == 'user' && event.actor_id.present?
123
-
124
- pf_audit_actor_type_label(type)
45
+ # Where "Unknown" comes from, said the same way wherever it shows (the
46
+ # event page, a record footer). Both already show "Unknown" as the actor
47
+ # right before it, so the explanation does not repeat the word.
48
+ def pf_audit_unknown_actor_hint
49
+ t('permission_forge.audit_events.show.unknown_actor_html', method: tag.code('PermissionForge.with_audit_actor'))
125
50
  end
126
51
 
52
+ # A type, category or source outside the catalogue is shown as stored.
127
53
  def pf_audit_actor_type_label(type)
128
- t("permission_forge.audit_log.actor_types.#{type}", default: type.to_s.humanize)
54
+ PermissionForge::Admin::ActorLabel.type_label(type)
129
55
  end
130
56
 
131
57
  def pf_audit_category_label(category)
132
- t("permission_forge.audit_log.categories.#{category}", default: category.to_s.humanize)
58
+ t("permission_forge.audit_log.categories.#{category}", default: category.to_s)
133
59
  end
134
60
 
135
61
  def pf_audit_source_label(source)
136
- t("permission_forge.audit_log.sources.#{source}", default: source.to_s.humanize)
62
+ t("permission_forge.audit_log.sources.#{source}", default: source.to_s)
137
63
  end
138
64
 
139
65
  def pf_audit_category_badge(category)
@@ -173,28 +99,40 @@ module PermissionForge
173
99
  # what the event says about it.
174
100
  def pf_audit_set_name(event, lookups)
175
101
  return nil if event.permission_set_id.blank? && event.permission_set_uuid.blank?
176
- return event.record_label if event.record_type == 'PermissionForge::PermissionSet' && event.record_label.present?
102
+ if event.record_type == 'PermissionForge::PermissionSet' && event.record_label.present?
103
+ return pf_record_name_text(event.record_label, :permission_set)
104
+ end
177
105
 
178
106
  name = lookups.permission_set_name(event)
179
- return name if name
107
+ return pf_record_name_text(name, :permission_set) if name
180
108
 
181
109
  t('permission_forge.audit_log.deleted_set', id: event.permission_set_id || event.permission_set_uuid)
182
110
  end
183
111
 
184
112
  def pf_audit_group_name(event, lookups)
185
113
  name = lookups.group_name(event)
186
- return name if name
114
+ return pf_record_name_text(name, :permission_set_group) if name
187
115
 
188
116
  id = PermissionForge::Admin::AuditLookups.group_id(event)
189
117
  id ? t('permission_forge.audit_log.deleted_group', id: id) : t('permission_forge.audit_log.unknown_group')
190
118
  end
191
119
 
192
- # What the event is about: its label snapshot, else type and id.
120
+ # What the event is about: its label snapshot (a set's, group's or
121
+ # rule's install-default name in the page's language: Admin::RecordName),
122
+ # else the type's label (Admin::AuditTrail::TYPES; a class outside them
123
+ # as stored) and id.
193
124
  def pf_audit_record_name(event)
194
- return event.record_label if event.record_label.present?
125
+ return pf_audit_label_text(event) if event.record_label.present?
195
126
  return '' if event.record_type.blank?
196
127
 
197
- [event.record_type.to_s.demodulize.titleize, event.record_id || event.record_uuid].compact.join(' #')
128
+ type = PermissionForge::Admin::AuditTrail::TYPES.key(event.record_type.to_s)
129
+ [type ? pf_audit_type_label(type) : event.record_type.to_s, event.record_id || event.record_uuid].compact.join(' #')
130
+ end
131
+
132
+ # An event's record label as Admin::RecordName reads a name of its type.
133
+ def pf_audit_label_text(event)
134
+ klass = PermissionForge::ModelLookup.model_for(event.record_type.to_s) if event.record_type.to_s.start_with?('PermissionForge::')
135
+ klass ? pf_record_name_text(event.record_label, klass) : event.record_label
198
136
  end
199
137
 
200
138
  # A value of a before/after or snapshot table: text as it is, nothing as
@@ -202,18 +140,18 @@ module PermissionForge
202
140
  def pf_audit_value(value)
203
141
  case value
204
142
  when nil then t('permission_forge.audit_log.no_value')
205
- when String then value.empty? ? t('permission_forge.audit_log.empty_text') : value
143
+ when String then value.empty? ? t('permission_forge.audit_log.empty_text') : pf_host_data(value)
206
144
  when true, false then t("permission_forge.audit_log.boolean.#{value}")
207
145
  when Numeric then value.to_s
208
- else ActiveSupport::JSON.encode(value)
146
+ else pf_host_data(ActiveSupport::JSON.encode(value))
209
147
  end
210
148
  end
211
149
 
212
- # One line per refused filter, in the admin's own words (EventQuery's
213
- # messages are Rails' errors.messages, which a host may not translate):
214
- # a value that is needed, or the value that is not valid.
150
+ # One line per refused filter, in the admin's own words: a value that
151
+ # is needed, or the value that is not valid. A filter outside the
152
+ # catalogue is named as stored.
215
153
  def pf_audit_filter_error(name, messages, value)
216
- field = t("permission_forge.audit_events.index.filters.#{name}", default: name.to_s.humanize)
154
+ field = t("permission_forge.audit_events.index.filters.#{name}", default: name.to_s)
217
155
  scope = 'permission_forge.audit_events.index.invalid'
218
156
  return t("#{scope}.blank", field: field) if Array(messages).include?(I18n.t(QUERY_BLANK_MESSAGE))
219
157
  return t("#{scope}.invalid", field: field) if value.blank?
@@ -0,0 +1,103 @@
1
+ module PermissionForge
2
+ # Labels and links for the rows of the Audit tabs' record lists (Person →
3
+ # Audit, Permission Set → Audit, under audit_trail_enabled) and their CSV
4
+ # exports: each record named as the admin names it on a page, as stored in
5
+ # the CSV.
6
+ #
7
+ # @api private
8
+ module AuditRecordsHelper
9
+ # [record label, where] per audited type; evaluated in the view
10
+ # context, `csv` true for the CSV export. On a page names and labels
11
+ # read as everywhere else in the admin: a set's, group's or rule's
12
+ # install-default name in the page's language (pf_record_name), a
13
+ # field's or action's generated label as no label (Admin::FieldLabel,
14
+ # Admin::ActionLabel; a verbatim name in <code>). The CSV keeps the
15
+ # values as stored (an export is data, as the log's exports keep a
16
+ # stored reason).
17
+ AUDIT_LABELS = {
18
+ 'permission_set' => ->(record, csv) { [pf_audit_name(record, csv), nil] },
19
+ 'group' => ->(record, csv) { [pf_audit_name(record, csv), nil] },
20
+ 'resource' => ->(record, csv) { [pf_resource_label(record), pf_audit_name(record.permission_set, csv)] },
21
+ 'field' => lambda { |record, csv|
22
+ label = csv ? pf_audit_stored_label(record) : pf_field_label(record.resource&.name, record.name, row: record)
23
+ [label, pf_audit_resource_where(record.resource, csv)]
24
+ },
25
+ 'action' => lambda { |record, csv|
26
+ [csv ? pf_audit_stored_label(record) : pf_audit_action_label(record), pf_audit_resource_where(record.resource, csv)]
27
+ },
28
+ 'rule' => ->(record, csv) { [pf_audit_name(record, csv), pf_audit_resource_where(record.resource, csv)] },
29
+ 'assignment' => ->(record, csv) { [pf_person_name(record.user), pf_audit_name(record.permission_set, csv)] },
30
+ 'group_membership' => ->(record, csv) { [pf_person_name(record.user), pf_audit_name(record.permission_set_group, csv)] }
31
+ }.freeze
32
+
33
+ # Link target per audited type; evaluated in the view context.
34
+ AUDIT_PATHS = {
35
+ 'permission_set' => ->(record) { permission_forge.permission_set_path(record) },
36
+ 'resource' => ->(record) { permission_forge.permission_set_resource_path(record.permission_set_id, record.name) },
37
+ 'field' => ->(record) { pf_audit_nested_path(record.resource, :field, record.name) },
38
+ 'action' => ->(record) { pf_audit_nested_path(record.resource, :action, record.id) },
39
+ 'rule' => ->(record) { pf_audit_nested_path(record.resource, :rule, record.id) },
40
+ 'assignment' => ->(record) { permission_forge.person_path(record.user_id) },
41
+ 'group' => ->(record) { permission_forge.group_path(record) },
42
+ 'group_membership' => ->(record) { permission_forge.group_path(record.permission_set_group_id) }
43
+ }.freeze
44
+
45
+ def pf_audit_type_label(type)
46
+ t("permission_forge.ui.audit.types.#{type}")
47
+ end
48
+
49
+ # [type label, record label, where] of a page's row (AUDIT_LABELS).
50
+ def pf_audit_describe(entry, csv: false)
51
+ label, where = pf_audit_lookup(AUDIT_LABELS, entry.type, entry.record, csv)
52
+ [pf_audit_type_label(entry.type), label, where]
53
+ end
54
+
55
+ # [type label, record label, where] of a CSV row: stored values.
56
+ def pf_audit_csv_describe(entry)
57
+ pf_audit_describe(entry, csv: true)
58
+ end
59
+
60
+ # "<set> › <model>" for a row under a Resource.
61
+ def pf_audit_resource_where(resource, csv)
62
+ return nil unless resource
63
+
64
+ "#{pf_audit_name(resource.permission_set, csv)} › #{pf_resource_label(resource)}"
65
+ end
66
+
67
+ # A set's, group's or rule's name: as the admin names it, or as stored for the CSV.
68
+ def pf_audit_name(record, csv)
69
+ csv ? record&.name : pf_record_name(record).presence
70
+ end
71
+
72
+ # A field's or action's label as stored, else its name (the CSV).
73
+ def pf_audit_stored_label(row)
74
+ row.label.presence || row.name
75
+ end
76
+
77
+ # An action's label (Admin::ActionLabel), a verbatim key in <code>.
78
+ def pf_audit_action_label(action)
79
+ label = PermissionForge::Admin::ActionLabel.for(action)
80
+ label.verbatim? ? tag.code(label.text, class: 'pf-mono-key') : label.text
81
+ end
82
+
83
+ def pf_audit_path(entry)
84
+ pf_audit_lookup(AUDIT_PATHS, entry.type, entry.record)
85
+ end
86
+
87
+ # Runs the registry entry for `type` in the view context; nil for unknown types.
88
+ def pf_audit_lookup(registry, type, *)
89
+ handler = registry[type]
90
+ instance_exec(*, &handler) if handler
91
+ end
92
+
93
+ def pf_audit_nested_path(resource, kind, key)
94
+ return nil unless resource
95
+
96
+ case kind
97
+ when :field then permission_forge.permission_set_resource_field_path(resource.permission_set_id, resource.name, key)
98
+ when :action then permission_forge.permission_set_resource_action_path(resource.permission_set_id, resource.name, key)
99
+ when :rule then permission_forge.edit_permission_set_resource_rule_path(resource.permission_set_id, resource.name, key)
100
+ end
101
+ end
102
+ end
103
+ end