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
@@ -1,138 +1,153 @@
1
1
  module PermissionForge
2
- # Plain-language sentences for activation conditions and record rules
3
- # (audit events: AuditSentencesHelper; "Review and save" change
4
- # descriptions: ChangesHelper).
2
+ # Plain-language sentences for record rules and resources (activation
3
+ # conditions: ConditionsHelper; audit events: AuditSentencesHelper;
4
+ # "Review and save" change descriptions: ChangesHelper).
5
5
  #
6
6
  # @api private
7
7
  module SentencesHelper
8
- NULLABLE_OPERATORS = %w[equal not_equal in not_in].freeze
9
- LIST_OPERATORS = %w[in not_in].freeze
10
- NO_VALUE_OPERATORS = %w[is_null is_not_null].freeze
11
- USER_OPERATORS = %w[equal not_equal in not_in is_null is_not_null].freeze
12
- CURRENT_USER = 'current_user'.freeze
13
-
14
- # --- activation conditions ------------------------------------------------
15
-
16
- def pf_condition_parts(permission_set)
17
- [pf_condition_ips(permission_set), pf_condition_hours(permission_set), pf_condition_flag(permission_set)].compact
18
- end
19
-
20
- def pf_condition_ips(permission_set)
21
- ips = pf_ip_list(permission_set.allowed_ips)
22
- return if ips.empty?
23
-
24
- connector = pf_ui('conditions.or')
25
- pf_ui('conditions.from_ips', ips: ips.to_sentence(two_words_connector: connector, last_word_connector: connector))
26
- end
27
-
28
- def pf_condition_hours(permission_set)
29
- from = permission_set.allowed_hours_start
30
- to = permission_set.allowed_hours_end
31
- return unless from.present? && to.present?
32
-
33
- key = to.to_i <= from.to_i ? 'hours_overnight' : 'hours'
34
- pf_ui("conditions.#{key}", from: pf_hour(from), to: pf_hour(to))
35
- end
36
-
37
- def pf_condition_flag(permission_set)
38
- flag = permission_set.requires_session_flag
39
- pf_flag_phrase(flag) if flag.present?
40
- end
41
-
42
- def pf_condition_sentence(permission_set)
43
- parts = pf_condition_parts(permission_set)
44
- return pf_ui('conditions.always') if parts.empty?
45
-
46
- pf_ui('conditions.only_works', parts: parts.join(', '))
47
- end
48
-
49
- def pf_ip_list(value)
50
- value.to_s.split(/[,\n]/).map(&:strip).reject(&:empty?)
8
+ # Resource flags a Muting Permission Set can take away, in the order the page shows them.
9
+ MUTED_FLAGS = %i[read_access create_access edit_access delete_access].freeze
10
+
11
+ # --- resources ------------------------------------------------------------
12
+
13
+ # The toast after "Add resource": how many fields it got. In a Muting
14
+ # Permission Set "with 0 fields" reads like a gap, so there it says what
15
+ # the resource takes away, or that it takes nothing away yet.
16
+ def pf_added_resource_notice(resource, muting:)
17
+ label = pf_resource_label(resource)
18
+ count = resource.resource_fields.size
19
+ return t('permission_forge.flash.resources.added', resource: label, count: count) unless muting
20
+
21
+ taken = pf_muted_parts(resource, count)
22
+ return t('permission_forge.flash.resources.added_muting_nothing', resource: label) if taken.empty?
23
+
24
+ t('permission_forge.flash.resources.added_muting', resource: label, what: pf_to_sentence(taken))
25
+ end
26
+
27
+ # What a muting resource takes away: its muted flags, then its field rows.
28
+ def pf_muted_parts(resource, field_count)
29
+ parts = MUTED_FLAGS.select { |flag| resource.public_send(flag) }.map { |flag| pf_ui("flags_short.#{flag}") }
30
+ parts << t('permission_forge.flash.resources.muted_fields', count: field_count) if field_count.positive?
31
+ parts
32
+ end
33
+
34
+ # --- field levels ---------------------------------------------------------
35
+
36
+ # What one Permission Set does to a field, by its level (an
37
+ # Admin::EffectiveAccess::FieldSource): "Sales rep shows this field masked (Email)."
38
+ # `set_name`: how the set is named (LabelsHelper#pf_chain_set_name on a
39
+ # person's effective access); the set's name by default.
40
+ def pf_field_source_sentence(source, set_name: pf_record_name(source.permission_set))
41
+ strategy = pf_mask_strategy_label(source.mask_strategy)
42
+ pf_t_markup(set: set_name) { |held| pf_ui("level_sentences.#{source.level}", **held, strategy: strategy) }
43
+ end
44
+
45
+ # A person's Effective access row for a field a Muting Permission Set
46
+ # decides (FieldOutcome#reason :muted): the level the deciding mutes
47
+ # give (FieldOutcome#deciding_mutes: hidden, masked or read-only) and
48
+ # their sets, each named with the Resource whose row mutes
49
+ # (#pf_field_source_set_name; B#38).
50
+ def pf_field_muted_by(field, leaf_name)
51
+ pf_t_markup(set: pf_deciding_mute_names(field, leaf_name)) do |held|
52
+ case field.blocked_level
53
+ when 'mask' then t('permission_forge.people.effective_field_source.masked_by', **held)
54
+ when 'no_edit' then t('permission_forge.people.effective_field_source.read_only_by', **held)
55
+ else t('permission_forge.people.effective_field_source.muted_by', **held)
56
+ end
57
+ end
51
58
  end
52
59
 
53
- def pf_flag_label(flag)
54
- configured = PermissionForge.configuration.admin_session_flags || {}
55
- configured[flag.to_s] || configured[flag.to_sym] ||
56
- t("permission_forge.ui.flags.#{flag}", default: flag.to_s.humanize)
60
+ # The field drawer's "Explain for" for the same decision: the level and
61
+ # the same sets as the row (#pf_field_muted_by), counted for the verb.
62
+ def pf_field_muted_explain(field, leaf_name, person_name)
63
+ count = field.deciding_mutes.size
64
+ pf_t_markup(sets: pf_deciding_mute_names(field, leaf_name)) do |held|
65
+ case field.blocked_level
66
+ when 'mask' then t('permission_forge.resource_fields.explain.masked_by', name: person_name, count: count, **held)
67
+ when 'no_edit' then t('permission_forge.resource_fields.explain.read_only_by', name: person_name, count: count, **held)
68
+ else t('permission_forge.resource_fields.explain.hidden_by', name: person_name, count: count, **held)
69
+ end
70
+ end
57
71
  end
58
72
 
59
- def pf_flag_phrase(flag)
60
- t("permission_forge.ui.conditions.flags.#{flag}",
61
- default: pf_ui('conditions.flag_generic', flag: pf_flag_label(flag)))
73
+ # The sets of FieldOutcome#deciding_mutes as one list, each named with its Resource.
74
+ def pf_deciding_mute_names(field, leaf_name)
75
+ pf_markup_sentence(field.deciding_mutes.map { |source| pf_field_source_set_name(source, leaf_name) })
62
76
  end
63
77
 
64
78
  # --- record rules ---------------------------------------------------------
65
79
 
66
80
  def pf_operator_label(operator)
67
- t("permission_forge.ui.operators.#{operator}", default: operator.to_s.humanize.downcase)
81
+ t("permission_forge.ui.operators.#{operator}", default: operator.to_s)
68
82
  end
69
83
 
70
84
  # Labels along a rule's relationship path and the final field, e.g.
71
- # ["Organization", "Members", "User"].
85
+ # ["Organization", "Members", "User"]. The path is walked by
86
+ # Operators::RulePath (the one walk of a path); one it cannot follow is
87
+ # labelled from its names (a rule that matches nothing is drawn by
88
+ # RuleProblemsHelper first).
72
89
  def pf_rule_field_labels(model_name, rule)
73
- klass = model_name.to_s.safe_constantize
74
- labels = rule.field_accessor_path.to_s.split('.').map do |assoc|
75
- label = pf_attribute_label(klass, assoc)
76
- klass = pf_association_class(klass, assoc)
77
- label
78
- end
79
- labels << pf_attribute_label(klass, rule.field_name)
90
+ segments = PermissionForge::Operators::RuleColumn.path_segments(rule.field_accessor_path)
91
+ models = pf_rule_path_models(PermissionForge::ModelLookup.model_for(model_name), segments)
92
+ labels = segments.each_with_index.map { |assoc, index| pf_attribute_label(models[index], assoc) }
93
+ labels << pf_attribute_label(models[segments.size], rule.field_name)
80
94
  end
81
95
 
96
+ # The label of a stored attribute or association name (Admin::FieldLabel),
97
+ # as text: the host's translation, else the name as stored.
82
98
  def pf_attribute_label(klass, attribute)
83
- klass ? klass.human_attribute_name(attribute) : attribute.to_s.humanize
99
+ pf_field_label_text(klass, attribute)
84
100
  end
85
101
 
86
- # Target class of an association, or nil (unknown or polymorphic).
87
- def pf_association_class(klass, assoc)
88
- reflection = klass&.reflect_on_association(assoc.to_sym)
89
- begin
90
- reflection&.klass
91
- rescue StandardError
92
- nil
93
- end
102
+ # The model, then the model of each association on the path; the model
103
+ # alone when the path cannot be followed (or nothing, without a model).
104
+ def pf_rule_path_models(klass, segments)
105
+ return [] unless klass.is_a?(Class) && klass < ActiveRecord::Base
106
+
107
+ PermissionForge::Operators::RulePath.follow(klass, segments).models.presence || [klass]
108
+ rescue *PermissionForge::Operators::RuleColumn::UNTELLABLE
109
+ [klass]
94
110
  end
95
111
 
112
+ # The value as the rule compares it: "the current user" only where
113
+ # ValueTyping substitutes the user's id (one value compared:
114
+ # ValueTyping.current_user?); in a list the word is an element as written.
96
115
  def pf_rule_value_text(rule)
97
116
  value = rule.field_value.to_s
98
117
  return '…' if value.strip.empty? # builder preview before a value is entered
99
- return pf_ui('rules.current_user') if value == CURRENT_USER
118
+ return pf_ui('rules.current_user') if PermissionForge::Operators::ValueTyping.current_user?(rule.operator, value)
100
119
 
101
120
  pf_rule_value_parts(rule.operator, value)
102
121
  end
103
122
 
104
- # List values as "a, b"; ranges as "a and b"; anything else verbatim.
123
+ # A list's values as "a, b"; a range's ends as "a and b" — split as
124
+ # ValueTyping splits them (its SHAPES: :list, :pair); anything else verbatim.
105
125
  def pf_rule_value_parts(operator, value)
106
- return value.split(',').map(&:strip).join(', ') if LIST_OPERATORS.include?(operator)
107
- return value.split(',', 2).map(&:strip).join(" #{pf_ui('rules.and')} ") if operator == 'between'
108
-
109
- value
126
+ parts = PermissionForge::Operators::ValueTyping.split(value)
127
+ case PermissionForge::Operators::ValueTyping::SHAPES[operator]
128
+ when :list then parts.join(', ')
129
+ when :pair then parts.join(" #{pf_ui('rules.and')} ")
130
+ else value
131
+ end
110
132
  end
111
133
 
134
+ # Whether the sentence adds "or … is empty": the rule includes the NULL
135
+ # rows and its operator's strategy adds them (Operators::Registry::NULL_INCLUDING).
112
136
  def pf_rule_nullable?(rule)
113
- rule.include_nulls && NULLABLE_OPERATORS.include?(rule.operator)
137
+ rule.include_nulls && PermissionForge::Operators::Registry::NULL_INCLUDING.include?(rule.operator)
114
138
  end
115
139
 
116
- # Plural model label. model_name.human(count: 2) falls back to the singular
117
- # when the host app has no model translations, so pluralize English then.
118
- def pf_resource_plural(name)
119
- klass = name.to_s.safe_constantize
120
- return name.to_s.demodulize.pluralize unless klass.respond_to?(:model_name)
121
-
122
- plural = klass.model_name.human(count: 2)
123
- plural == klass.model_name.human && I18n.locale.to_s == 'en' ? plural.pluralize : plural
124
- end
125
-
126
- def pf_resource_label(resource_or_name)
127
- name = resource_or_name.respond_to?(:name) ? resource_or_name.name : resource_or_name.to_s
128
- return resource_or_name.label if resource_or_name.respond_to?(:label) && resource_or_name.label.present?
129
-
130
- klass = name.safe_constantize
131
- klass.respond_to?(:model_name) ? klass.model_name.human : name.demodulize.titleize
140
+ # Whether the operator takes no value (Operators::Validations::VALUELESS: the null checks).
141
+ def pf_rule_valueless?(rule)
142
+ PermissionForge::Operators::Validations.valueless?(rule.operator)
132
143
  end
133
144
 
134
- # Token sentence: Only show [Customers] where [Owner] [is] [the current user], or Owner is empty
145
+ # Token sentence: Only show [Customers] where [Owner] [is] [the current user], or Owner is empty.
146
+ # A rule that matches nothing says so, and what it names as written.
135
147
  def pf_rule_sentence(rule, model_name = rule.resource&.name)
148
+ problem = pf_rule_problem(rule)
149
+ return pf_rule_problem_sentence(rule, problem) if problem
150
+
136
151
  field = pf_rule_field_labels(model_name, rule).join(' › ')
137
152
  sentence = safe_join(pf_rule_tokens(rule, model_name, field), ' ')
138
153
  sentence = safe_join([sentence, content_tag(:span, ", #{pf_ui('rules.or_empty', field: field)}")]) if pf_rule_nullable?(rule)
@@ -151,24 +166,30 @@ module PermissionForge
151
166
  end
152
167
 
153
168
  def pf_rule_value_token(rule)
154
- return if NO_VALUE_OPERATORS.include?(rule.operator)
169
+ return if pf_rule_valueless?(rule)
155
170
 
156
171
  content_tag(:span, pf_rule_value_text(rule), class: 'pf-token pf-token--value')
157
172
  end
158
173
 
159
174
  def pf_rule_text(rule, model_name = rule.resource&.name)
175
+ problem = pf_rule_problem(rule)
176
+ return pf_rule_problem_line(problem) if problem
177
+
160
178
  field = pf_rule_field_labels(model_name, rule).join(' › ')
161
179
  text = "#{pf_ui('rules.only_show')} #{pf_resource_plural(model_name)} #{pf_ui('rules.where')} #{field} " \
162
180
  "#{pf_operator_label(rule.operator)}"
163
- text += " #{pf_rule_value_text(rule)}" unless NO_VALUE_OPERATORS.include?(rule.operator)
181
+ text += " #{pf_rule_value_text(rule)}" unless pf_rule_valueless?(rule)
164
182
  text += ", #{pf_ui('rules.or_empty', field: field)}" if pf_rule_nullable?(rule)
165
183
  text
166
184
  end
167
185
 
168
186
  # Short form used in matrix cells: "Owner is the current user".
169
187
  def pf_rule_summary(rule, model_name = rule.resource&.name)
188
+ problem = pf_rule_problem(rule)
189
+ return safe_join(['⊘ ', pf_rule_problem_line(problem)]) if problem
190
+
170
191
  field = pf_rule_field_labels(model_name, rule).join(' › ')
171
- value = NO_VALUE_OPERATORS.include?(rule.operator) ? nil : pf_rule_value_text(rule)
192
+ value = pf_rule_valueless?(rule) ? nil : pf_rule_value_text(rule)
172
193
  [field, pf_operator_label(rule.operator), value].compact.join(' ')
173
194
  end
174
195
  end
@@ -54,7 +54,7 @@ module PermissionForge
54
54
  options << row.value unless options.include?(row.value.to_s)
55
55
  pills = options.map do |opt|
56
56
  current = opt == row.value.to_s
57
- content_tag(:span, opt, class: "pf-pill pf-pill--opt#{' pf-pill--current' if current}", 'aria-current': (current ? 'true' : nil))
57
+ content_tag(:code, opt, class: "pf-pill pf-pill--opt#{' pf-pill--current' if current}", 'aria-current': (current ? 'true' : nil))
58
58
  end
59
59
  content_tag(:span, safe_join(pills, ' '), class: 'pf-row pf-row--tight')
60
60
  end
@@ -88,7 +88,7 @@ module PermissionForge
88
88
  value = row.value
89
89
  return content_tag(:span, t('permission_forge.settings.values.empty'), class: 'pf-text-3') if value.blank?
90
90
 
91
- content_tag(:span, safe_join(value.map { |k, v| content_tag(:span, "#{k} → #{v}") }), class: 'pf-map')
91
+ content_tag(:span, safe_join(value.map { |k, v| content_tag(:code, "#{k} → #{v}") }), class: 'pf-map')
92
92
  end
93
93
 
94
94
  def pf_setting_secret(row)
@@ -6,8 +6,6 @@ module PermissionForge
6
6
  #
7
7
  # @api private
8
8
  module UiHelper
9
- STATE_GLYPHS = { granted: '✓', blocked: '⊘', none: '–' }.freeze
10
-
11
9
  # --- assets ---------------------------------------------------------------
12
10
 
13
11
  def pf_asset_url(file)
@@ -41,6 +39,40 @@ module PermissionForge
41
39
  t("permission_forge.ui.#{key}", **)
42
40
  end
43
41
 
42
+ # A translation whose interpolated values may be markup (a SafeBuffer:
43
+ # a name marked as host data) although its key does not end in `_html`:
44
+ # the block translates with a marker in place of each value, and the
45
+ # pieces are joined with safe_join — the translation's own text escaped,
46
+ # the markup kept, nothing marked safe by hand. The `t(...)` stays in the
47
+ # caller (i18n-tasks sees the key).
48
+ #
49
+ # pf_t_markup(set: pf_host_data(name)) { |held| t('.flag_granted', **held, flag: word) }
50
+ #
51
+ # A marker no value was given for (text that happens to hold one, such
52
+ # as a stored value interpolated as text) is left as written, escaped.
53
+ #
54
+ # @yieldparam held [Hash{Symbol => String}] the markers, by value name
55
+ # @return [ActiveSupport::SafeBuffer]
56
+ def pf_t_markup(**values)
57
+ held = values.to_h { |name, _| [name, "\u0001#{name}\u0001"] }
58
+ by_name = values.transform_keys(&:to_s)
59
+ pieces = yield(held).to_s.split(/\u0001(\w+)\u0001/)
60
+ safe_join(pieces.each_with_index.map { |piece, index| index.odd? ? by_name.fetch(piece) { "\u0001#{piece}\u0001" } : piece })
61
+ end
62
+
63
+ # "Bob, Carol and Dan" in the page's language (Admin::ListSentence): never
64
+ # a bare to_sentence, whose connectors stay English without rails-i18n.
65
+ def pf_to_sentence(items, **)
66
+ PermissionForge::Admin::ListSentence.call(items, **)
67
+ end
68
+
69
+ # #pf_to_sentence for items that carry markup (a set named with its
70
+ # Resource as host data): each item is kept as it is (#pf_t_markup).
71
+ def pf_markup_sentence(items)
72
+ values = items.each_with_index.to_h { |item, index| [:"item#{index}", item] }
73
+ pf_t_markup(**values) { |held| pf_to_sentence(held.values) }
74
+ end
75
+
44
76
  def pf_people_text(count)
45
77
  pf_ui('people_count', count: count)
46
78
  end
@@ -79,11 +111,28 @@ module PermissionForge
79
111
  format('%02d:00', hour.to_i)
80
112
  end
81
113
 
114
+ # --- states (Admin::StateLabel: one registry of glyphs and words) -----------
115
+
116
+ # A state (Admin::StateLabel::Label): `.word`, `.glyph`, and `.to_s` for
117
+ # both — pf_state(:field_level, :none).word → "Not configured",
118
+ # pf_state(:effective, :blocked).to_s → "⊘ Blocked".
119
+ def pf_state(kind, state)
120
+ PermissionForge::Admin::StateLabel.for(kind, state)
121
+ end
122
+
123
+ # The glyph (hidden from assistive technology) and the word of a state
124
+ # (pf_state), for a cell (`<span class="pf-cell__glyph">`) or a legend
125
+ # (`glyph_tag: :b`, one space between) that styles the glyph itself.
126
+ def pf_state_parts(label, glyph_tag: :span, separator: '')
127
+ glyph = content_tag(glyph_tag, label.glyph, class: ('pf-cell__glyph' if glyph_tag == :span), 'aria-hidden': true)
128
+ safe_join([glyph, label.word], separator)
129
+ end
130
+
82
131
  # Read-only state cell (effective access grid, across-sets lists).
83
132
  def pf_state_cell(state, aria:, href: nil, why: nil, current: false)
84
133
  state = state.to_sym
85
- label = pf_ui("states.#{state}")
86
- content = safe_join([content_tag(:span, STATE_GLYPHS.fetch(state), class: 'pf-cell__glyph', 'aria-hidden': true),
134
+ label = pf_state(:effective, state).word
135
+ content = safe_join([content_tag(:span, pf_state(:effective, state).glyph, class: 'pf-cell__glyph', 'aria-hidden': true),
87
136
  content_tag(:span, label)], ' ')
88
137
  opts = { class: "pf-state pf-state--#{state}", 'aria-label': "#{aria}: #{label}" }
89
138
  opts[:'aria-current'] = 'true' if current
@@ -135,6 +184,18 @@ module PermissionForge
135
184
  content_tag(pf_fragment? ? :h3 : :h2, text, class: 'pf-h3', id: id)
136
185
  end
137
186
 
187
+ # A frame that scrolls sideways when its content (a wide grid) is wider than the page: a
188
+ # tab stop with a name (aria-label, or aria-labelledby on a section), so the keyboard
189
+ # reaches the region and assistive technology says what scrolls; the stylesheet paints the
190
+ # cue at its edges and clips what it contains. Every grid 480px or wider goes in one
191
+ # (html/layout_lint_test.rb).
192
+ # <%= pf_scroll_frame('aria-label': t('.label'), class: 'pf-card') do %> … <% end %>
193
+ # <%= pf_scroll_frame(tag: :section, 'aria-labelledby': 'pf-changes', class: 'pf-card') do %> … <% end %>
194
+ def pf_scroll_frame(tag: :div, **options, &)
195
+ classes = ['pf-scroll-x', options.delete(:class)].compact.join(' ')
196
+ content_tag(tag, class: classes, role: (tag == :section ? nil : 'region'), tabindex: '0', **options, &)
197
+ end
198
+
138
199
  # Breadcrumb from [[label, path], ..., label]
139
200
  def pf_breadcrumb(*items)
140
201
  parts = items.map do |item|
@@ -25,6 +25,10 @@ module PermissionForge
25
25
  ACTOR_TYPES = PermissionForge::Audit::Actor::TYPES
26
26
  SOURCES = PermissionForge::Audit::Source::SOURCES
27
27
 
28
+ # actor_label and record_label name people: #inspect hides them whenever
29
+ # the application's filters hide a `name` (InspectionFilters).
30
+ PermissionForge::InspectionFilters.hide_people_labels(self)
31
+
28
32
  # JSON text that works on SQLite, PostgreSQL and MySQL alike; times are
29
33
  # written in ISO 8601 (ActiveSupport::JSON), nothing reads as nil.
30
34
  #
@@ -68,7 +72,7 @@ module PermissionForge
68
72
  # @return [Symbol, nil]
69
73
  def self.tenant_column
70
74
  column = PermissionForge.configuration.resolved_tenant_column
71
- column if column && column_names.include?(column.to_s)
75
+ column if column && PermissionForge::SchemaRead.column?(self, column.to_s, ignored: false)
72
76
  end
73
77
 
74
78
  # The events of one tenant, on the relation it is called on; every event