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
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ # Whether a ResourceField name names something on the Resource's model —
5
+ # what `rake permission_forge:audit` reports (integrity/unknown_fields): a
6
+ # typo such as `emial` configures nothing the model has. Saving does not
7
+ # refuse such a name: FiltersResponseAttributes filters any key of a hash
8
+ # (a key only a serializer computes is configured by a row of its name).
9
+ # A name is a field of the model when it is
10
+ #
11
+ # - a column of its table (a model's `ignored_columns` included), read
12
+ # again from the database before the name is called unknown
13
+ # (PermissionForge::SchemaRead): the schema cache may predate a column a
14
+ # migration just added;
15
+ # - an attribute (`attribute :nickname`) or an `alias_attribute`;
16
+ # - a virtual field: declared (extra_fields_method, also a key only a
17
+ # serializer computes) or discovered (store accessors, attribute-API
18
+ # attributes) — PermissionForge::VirtualFields;
19
+ # - a public reader or writer (a store accessor with a custom prefix, a
20
+ # `delegate … prefix: true`, `has_secure_password`'s `password=`),
21
+ # as FieldVariants counts one; or
22
+ # - a configured variant (config.field_variant_mappings) of one of those
23
+ # or of another row of the Resource: `owner_uuid` next to `owner_id`,
24
+ # the key a serializer computes and FiltersResponseAttributes reads by
25
+ # its base field (FieldVariants.base_entries).
26
+ #
27
+ # Any of those on a loaded single-table-inheritance subclass of the model
28
+ # (one on the same table) counts too: a row on the base Resource reaches
29
+ # the subclass's records (ResourceChain), so `vip_level`, a method only
30
+ # VipCustomer defines, is a field of Customer's rows. A subclass not
31
+ # loaded yet (development, before it is referenced) is not seen.
32
+ #
33
+ # Nothing is said (.for returns nil) when the model cannot be told: the
34
+ # Resource names no Active Record model this app can load (a Resource
35
+ # shared with another app), an abstract class, a table that does not exist
36
+ # (yet), or a database that cannot be asked.
37
+ #
38
+ # @example
39
+ # names = PermissionForge::FieldNames.for('Customer')
40
+ # names.include?('email') # => true
41
+ # names.include?('emial') # => false
42
+ #
43
+ # @api private
44
+ class FieldNames
45
+ # Errors that mean the model cannot be told now.
46
+ UNREADABLE = [NameError, ArgumentError, ActiveRecord::ActiveRecordError].freeze
47
+
48
+ # @param resource_name [String, Symbol, nil] the Resource's name
49
+ # @param siblings [Array<String>, #call] the names of the Resource's other
50
+ # rows (variant bases), or what answers them (asked only when needed)
51
+ # @return [FieldNames, nil] nil when the model cannot be told
52
+ def self.for(resource_name, siblings: [], configuration: PermissionForge.configuration)
53
+ model = ModelLookup.model_for(resource_name)
54
+ return unless model
55
+
56
+ new(model, siblings: siblings, configuration: configuration)
57
+ rescue *UNREADABLE
58
+ nil
59
+ end
60
+
61
+ # An attribute, an alias, or a public reader or writer of the model —
62
+ # the one answer to "the model has this name" (FieldNames, FieldVariants).
63
+ # A name the table has no column for is asked again after the schema is
64
+ # read again, once (PermissionForge::SchemaRead.has?): a column a
65
+ # migration just added is an attribute.
66
+ #
67
+ # @param model [Class]
68
+ # @param name [String]
69
+ # @return [Boolean]
70
+ def self.declares?(model, name)
71
+ name = name.to_s
72
+ SchemaRead.has?(model, name) do
73
+ model.attribute_names.include?(name) || model.attribute_alias?(name) || model.public_method_defined?(name) ||
74
+ model.public_method_defined?("#{name}=")
75
+ end
76
+ end
77
+
78
+ # @return [Class] the Resource's model
79
+ attr_reader :model
80
+
81
+ def initialize(model, siblings: [], configuration: PermissionForge.configuration)
82
+ @model = model
83
+ @siblings = siblings
84
+ @configuration = configuration
85
+ end
86
+
87
+ # @param name [String, Symbol]
88
+ # @return [Boolean] whether the model has the name (its table read again,
89
+ # once, before saying no: PermissionForge::SchemaRead)
90
+ def include?(name)
91
+ name = name.to_s
92
+ SchemaRead.has?(@model, name) { known?(name) }
93
+ rescue *UNREADABLE
94
+ true
95
+ end
96
+
97
+ private
98
+
99
+ def known?(name)
100
+ forget_stale_memos
101
+ own?(name) || variant?(name)
102
+ end
103
+
104
+ # The subclasses and virtual fields as of the schema the cache holds now:
105
+ # a re-read (SchemaRead) resets the model's attributes.
106
+ def forget_stale_memos
107
+ reads = SchemaRead.rereads
108
+ return if @reads == reads
109
+
110
+ @reads = reads
111
+ @virtual_fields = nil
112
+ @models = nil
113
+ end
114
+
115
+ def own?(name)
116
+ columns.include?(name) || models.any? { |model| virtual_fields(model).include?(name) || self.class.declares?(model, name) }
117
+ end
118
+
119
+ def variant?(name)
120
+ bases = models.flat_map { |model| model.attribute_names + virtual_fields(model).names }
121
+ (columns + bases + siblings).uniq.any? do |base|
122
+ base != name && @configuration.field_variants_for(base).include?(name)
123
+ end
124
+ end
125
+
126
+ # The model and its loaded subclasses on the same table (STI).
127
+ def models
128
+ @models ||= [@model] + @model.descendants.select { |sub| sub.table_name == @model.table_name }
129
+ end
130
+
131
+ def siblings
132
+ @siblings = Array(@siblings.respond_to?(:call) ? @siblings.call : @siblings).map(&:to_s)
133
+ end
134
+
135
+ # The table's columns as the database has them: ignored_columns too.
136
+ def columns
137
+ SchemaRead.columns(@model).keys
138
+ end
139
+
140
+ def virtual_fields(model)
141
+ @virtual_fields ||= {}
142
+ @virtual_fields[model] ||= VirtualFields.new(model, @configuration)
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ # The names a field row's setting also applies to on one Resource:
5
+ # Configuration#field_variants_for (config.field_variant_mappings:
6
+ # `owner_id` → `owner_uuid`, `price_cents` → `price`), kept to the names
7
+ # the Resource's model has —
8
+ #
9
+ # - an attribute (a column, or one declared with `attribute`),
10
+ # - an `alias_attribute`,
11
+ # - a public reader or writer (a `delegate :uuid, to: :owner, prefix: true`
12
+ # defines `owner_uuid`; `def owner_uuid=(uuid)` takes one), or
13
+ # - a virtual field, declared or discovered (PermissionForge::VirtualFields#include?)
14
+ #
15
+ # — the answer FieldNames gives (FieldNames.declares?, one definition). A
16
+ # name the table has no column for is asked again after the schema is
17
+ # read again, once (PermissionForge::SchemaRead): a variant column a
18
+ # migration just added gets its entry.
19
+ #
20
+ # A name the model does not have would be an entry in the permission hash
21
+ # for nothing (the admin's Effective access listed it as a field and
22
+ # counted it). A model that cannot be read — no such constant (a Resource
23
+ # shared with another app), not an Active Record model, a schema that
24
+ # cannot be loaded — keeps every variant, as before 0.15.
25
+ #
26
+ # A hash a serializer builds can still carry such a name (`owner_uuid`
27
+ # computed in a view): FiltersResponseAttributes reads it by the entry of
28
+ # the field it is a variant of (.base_entries), as when the hash had it.
29
+ #
30
+ # The one definition for the permission hash (granting and muting,
31
+ # PermissionHashBuilder / FieldPermissionMerger) and the admin
32
+ # (Admin::EffectiveFields, Admin::FieldDetail), which must agree.
33
+ #
34
+ # @example
35
+ # variants = PermissionForge::FieldVariants.new('Customer')
36
+ # variants.call('supplier_id') # => [] (Customer has no supplier_uuid)
37
+ #
38
+ # @api private
39
+ class FieldVariants
40
+ # Errors that mean the model's attributes cannot be read (no connection,
41
+ # a schema that cannot be loaded).
42
+ UNREADABLE = [ActiveRecord::ActiveRecordError].freeze
43
+
44
+ # The entries of the configured fields by the names of their variants
45
+ # (`owner_uuid` → the `owner_id` entry), to read a name that has no entry
46
+ # of its own. A name two fields map to (custom field_variant_mappings)
47
+ # gets the strictest of their entries (ResourceChain.strictest, the one
48
+ # definition): such a key is never more readable than any of its bases.
49
+ # This covers names the model does
50
+ # not have (serializer keys); a variant the model has gets its entry from
51
+ # FieldPermissionMerger, which merges every base row like any granting row.
52
+ #
53
+ # @param fields [Hash] a resource's field entries, by name
54
+ # @return [Hash{String => Hash}]
55
+ def self.base_entries(fields, configuration = PermissionForge.configuration)
56
+ bases = fields.each_with_object({}) do |(base, entry), by_variant|
57
+ configuration.field_variants_for(base).each { |variant| (by_variant[variant] ||= []) << entry }
58
+ end
59
+ bases.transform_values { |entries| ResourceChain.strictest(entries) }
60
+ end
61
+
62
+ # @param resource_name [String, Symbol] the Resource's name (a model's class name)
63
+ # @param configuration [Configuration]
64
+ def initialize(resource_name, configuration = PermissionForge.configuration)
65
+ @resource_name = resource_name.to_s
66
+ @configuration = configuration
67
+ @variants = {}
68
+ end
69
+
70
+ # @param field_name [String, Symbol]
71
+ # @return [Array<String>] the variants of the field that the model has
72
+ def call(field_name)
73
+ @variants[field_name.to_s] ||= @configuration.field_variants_for(field_name).select { |name| attribute?(name) }
74
+ end
75
+
76
+ private
77
+
78
+ def attribute?(name)
79
+ return true unless model
80
+
81
+ FieldNames.declares?(model, name) || virtual_fields.include?(name)
82
+ rescue *UNREADABLE
83
+ true
84
+ end
85
+
86
+ def model
87
+ return @model if defined?(@model)
88
+
89
+ @model = ModelLookup.model_for(@resource_name)
90
+ end
91
+
92
+ def virtual_fields
93
+ @virtual_fields ||= VirtualFields.new(model, @configuration)
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ # The one definition of "this stored text is a default the gem generated",
5
+ # not something an admin wrote. Through 0.15 the gem stored English it had
6
+ # made up — a field or action row's label (`name.humanize`, the install
7
+ # migration's 'Untitled Field'), a rule's blocked message and the names of
8
+ # a set, a group or a rule created without one (the install migration's
9
+ # column defaults) — and showed it as if an admin had typed it, in every
10
+ # language. The writers store the name itself now and the models store
11
+ # nothing for a message or name nobody wrote; the rows stored before keep
12
+ # their text (no data is rewritten), and whatever displays one asks here
13
+ # and renders the locale's own words instead.
14
+ #
15
+ # Asked by PermissionRuleApplier.failure_message_for (a denial's
16
+ # message), the admin's labels (Admin::FieldLabel, Admin::ActionLabel: a
17
+ # generated label is no label), names (Admin::RecordName: an install
18
+ # default is named in the page's language) and form pre-fills
19
+ # (Admin::Prefill: blank for generated text).
20
+ #
21
+ # @example
22
+ # GeneratedText.label?('Owner', 'owner_id') # => true
23
+ # GeneratedText.failure_message?('You do not have permission to perform this action.') # => true
24
+ # GeneratedText.name?('Untitled Rule', :permission_set_rule) # => true
25
+ #
26
+ # @api private
27
+ module GeneratedText
28
+ # The label the install migration gives a field row (the column default).
29
+ UNTITLED_FIELD = 'Untitled Field'
30
+ # The names the install migration gave a set, a group and a rule created
31
+ # without one (the column defaults of installs made before 0.15), by kind
32
+ # (the model's `model_name.element`).
33
+ NAMES = {
34
+ permission_set: 'Untitled Permission Set',
35
+ permission_set_group: 'Untitled Permission Set Group',
36
+ permission_set_rule: 'Untitled Rule'
37
+ }.freeze
38
+ # The blocked messages the install migration gave a rule created without
39
+ # one (the column default of installs made before 0.15).
40
+ FAILURE_MESSAGES = ['You do not have permission to perform this action.'].freeze
41
+
42
+ module_function
43
+
44
+ # Whether a field or action row's label is nothing an admin wrote: blank,
45
+ # the name itself, the name humanized (what the gem stored through 0.15)
46
+ # or the install default. A label an admin typed that happens to equal
47
+ # the humanized name reads the same as one the gem generated — and as
48
+ # what Rails' own English would show for it.
49
+ #
50
+ # @param text [String, nil] the stored label
51
+ # @param name [String, Symbol] the row's name (a column, a virtual field, an action key)
52
+ # @return [Boolean]
53
+ def label?(text, name)
54
+ label = text.to_s.strip
55
+ return true if label.empty? || label == UNTITLED_FIELD
56
+
57
+ name = name.to_s
58
+ label == name || label == name.to_s.humanize
59
+ end
60
+
61
+ # Whether a rule's blocked message is the install migration's default.
62
+ #
63
+ # @param text [String, nil] the stored failure_message
64
+ # @return [Boolean]
65
+ def failure_message?(text)
66
+ FAILURE_MESSAGES.include?(text.to_s.strip)
67
+ end
68
+
69
+ # Whether a set's, group's or rule's name is the install migration's
70
+ # default for its kind.
71
+ #
72
+ # @param text [String, nil] the stored name
73
+ # @param kind [Symbol, Class, ActiveRecord::Base] :permission_set,
74
+ # :permission_set_group or :permission_set_rule — or the model (class or record)
75
+ # @return [Boolean]
76
+ def name?(text, kind)
77
+ NAMES[kind_of(kind)] == text.to_s.strip
78
+ end
79
+
80
+ # Every text the install migration has given a column of the nine tables
81
+ # (an upgraded install still has them as column defaults): what no form
82
+ # may pre-fill.
83
+ #
84
+ # @return [Array<String>]
85
+ def install_defaults
86
+ [UNTITLED_FIELD, *NAMES.values, *FAILURE_MESSAGES]
87
+ end
88
+
89
+ def kind_of(kind)
90
+ kind = kind.class if kind.is_a?(ActiveRecord::Base)
91
+ kind.respond_to?(:model_name) ? kind.model_name.element.to_sym : kind.to_s.to_sym
92
+ end
93
+ private_class_method :kind_of
94
+ end
95
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ # What #inspect hides on the gem's own models. Rails copies
5
+ # config.filter_parameters into ActiveRecord::Base.filter_attributes, so
6
+ # the GDPR patterns PermissionForge adds there (filter_parameters_integration:
7
+ # `\Aname\z`, …) also hid the *configuration* names of the gem's rows: a
8
+ # console printed `#<PermissionForge::PermissionSet … name: [FILTERED]>` for a
9
+ # set called "Sales rep", and a ResourceField's `name` (a column name such as
10
+ # "email") the same way.
11
+ #
12
+ # Each model sets its own list with Rails' `filter_attributes=` as its
13
+ # class body runs (after config/initializers, again after a reload):
14
+ #
15
+ # - .show_configuration_names: the inherited filters, each made unable to
16
+ # match the model's configuration-name attributes (`name`, `label`,
17
+ # `field_name`) — they keep hiding every other attribute they matched (a
18
+ # rule's `field_value` or `failure_message`, a description). A Proc filter
19
+ # is kept as it is: it decides for itself.
20
+ # - .hide_people_labels (AuditEvent): the inherited filters plus the
21
+ # labels that name people (`actor_label`, `record_label`, which
22
+ # DataSubjectErasure scrubs), whenever the inherited filters hide a
23
+ # person's `name` — with the GDPR patterns, or a host's own `:name`.
24
+ #
25
+ # Only Regexps are added, so Rails 8's FilterAttributeHandler (which copies a
26
+ # model's String and Symbol entries back into config.filter_parameters as
27
+ # "model.attribute") adds nothing to the host's request-parameter filter.
28
+ # Request parameters, SQL bind logging (ActiveRecord::Base's filter) and the
29
+ # host's models are untouched. A filter added to
30
+ # ActiveRecord::Base.filter_attributes after a model has loaded does not
31
+ # reach it until the next reload.
32
+ #
33
+ # @api private
34
+ module InspectionFilters
35
+ # The attributes of the gem's models that hold configuration names.
36
+ CONFIGURATION_NAMES = %w[name label field_name].freeze
37
+ # AuditEvent's labels that name a person.
38
+ PEOPLE_LABELS = %w[actor_label record_label].freeze
39
+
40
+ module_function
41
+
42
+ # @param klass [Class] a model of the gem
43
+ # @return [void]
44
+ def show_configuration_names(klass, names = CONFIGURATION_NAMES)
45
+ klass.filter_attributes = sparing(klass.superclass.filter_attributes, names)
46
+ end
47
+
48
+ # @param klass [Class] PermissionForge::AuditEvent
49
+ # @return [void]
50
+ def hide_people_labels(klass, labels = PEOPLE_LABELS)
51
+ inherited = klass.superclass.filter_attributes
52
+ return unless hides?(inherited, 'name')
53
+
54
+ # The inherited entries as Regexps too (sparing nothing): a String or
55
+ # Symbol here would be copied into config.filter_parameters.
56
+ klass.filter_attributes = sparing(inherited, []) + [/\A(?:#{labels.map { |label| Regexp.escape(label) }.join('|')})\z/]
57
+ end
58
+
59
+ # `filters` (a filter_attributes list) with every String, Symbol and
60
+ # Regexp turned into a Regexp that matches what it matched, except the
61
+ # attribute names `names` themselves (none when `names` is empty). A
62
+ # String or Symbol matches as Rails reads it: anywhere in the name,
63
+ # ignoring case.
64
+ #
65
+ # @return [Array]
66
+ def sparing(filters, names)
67
+ guard = names.empty? ? '' : "\\A(?!(?:#{names.map { |name| Regexp.escape(name.to_s) }.join('|')})\\z).*?"
68
+ filters.map do |filter|
69
+ case filter
70
+ when Proc then filter
71
+ when Regexp
72
+ # Under /x a trailing comment would swallow the closing parenthesis.
73
+ Regexp.new("#{guard}(?:#{filter.source}#{"\n" if filter.options.anybits?(Regexp::EXTENDED)})", filter.options)
74
+ else Regexp.new("#{guard}(?:#{Regexp.escape(filter.to_s)})", Regexp::IGNORECASE)
75
+ end
76
+ end
77
+ end
78
+
79
+ def hides?(filters, attribute)
80
+ ActiveSupport::ParameterFilter.new(filters).filter_param(attribute, 'x') != 'x'
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ # The one definition of the fields a model's Resource has rows for: its
5
+ # columns (`ignored_columns` and config.ignored_fields left out), then its
6
+ # virtual fields (PermissionForge::VirtualFields: declared and
7
+ # discovered), whatever extra_fields_default_access says. Every path that
8
+ # creates rows for a model writes rows for these names and no others —
9
+ # the admin's presets, "Sync fields" and bulk add (a posted name outside
10
+ # them is no row), and ResourceFieldGenerator — so a level chosen for "all
11
+ # fields" (Hidden, say) reaches a virtual field on every path. The admin's
12
+ # model catalog lists them (Admin::ModelCatalog::ModelInfo#managed_field_names).
13
+ #
14
+ # The columns are read through PermissionForge::SchemaRead: compared with
15
+ # the database first with `verify: true` (a list that builds rows), from
16
+ # the schema cache without it (a count on every admin page).
17
+ #
18
+ # @api private
19
+ class ManagedFields
20
+ # @param model [Class] an Active Record model with a table
21
+ # @param configuration [Configuration]
22
+ # @param verify [Boolean] compare the columns with the database first
23
+ def initialize(model, configuration: PermissionForge.configuration, verify: true)
24
+ @model = model
25
+ @configuration = configuration
26
+ @verify = verify
27
+ end
28
+
29
+ # @return [Array<String>] the model's columns (`ignored_columns` excluded)
30
+ def column_names
31
+ @column_names ||= SchemaRead.column_names(@model, verify: @verify)
32
+ end
33
+
34
+ # @return [Array<String>] the columns config.ignored_fields leaves out
35
+ def ignored_column_names
36
+ column_names & ignored_fields
37
+ end
38
+
39
+ # @return [Array<String>] the columns that get rows
40
+ def managed_column_names
41
+ column_names - ignored_fields
42
+ end
43
+
44
+ # @return [Array<String>] declared and discovered virtual fields
45
+ def virtual_field_names
46
+ virtual_fields.names
47
+ end
48
+
49
+ # @return [Array<String>] managed columns, then virtual fields
50
+ def names
51
+ managed_column_names + virtual_field_names
52
+ end
53
+
54
+ # @return [VirtualFields]
55
+ def virtual_fields
56
+ @virtual_fields ||= VirtualFields.new(@model, @configuration)
57
+ end
58
+
59
+ private
60
+
61
+ # Symbols or Strings, as the host wrote them.
62
+ def ignored_fields
63
+ @ignored_fields ||= Array(@configuration.ignored_fields).map(&:to_s)
64
+ end
65
+ end
66
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'database_reach'
4
+
3
5
  module PermissionForge
4
6
  # Auto-applied masking: mixes MaskedAttributes into the models
5
7
  # config.auto_apply_masked_attributes selects and wraps the virtual readers
@@ -44,11 +46,12 @@ module PermissionForge
44
46
  def apply(only: nil)
45
47
  config = PermissionForge.configuration
46
48
  return true unless enabled?(config)
47
- return false unless PermissionForge::Resource.table_exists?
49
+ # No database to use (not configured, unreachable), or no table yet.
50
+ return false unless DatabaseReach.table_exists?(PermissionForge::Resource)
48
51
 
49
52
  apply_selected(config, only).all? & unknown_listed_names(config, only).empty?
50
- rescue ActiveRecord::StatementInvalid, ActiveRecord::ConnectionNotEstablished
51
- # Tables may not exist yet during early boot or pre-migration.
53
+ rescue *DatabaseReach::READ_ERRORS
54
+ # The database went away while the rows were read.
52
55
  false
53
56
  end
54
57
 
@@ -162,8 +165,9 @@ module PermissionForge
162
165
  def warn_unknown(setting, name)
163
166
  return if unmaskable_warned.put_if_absent("#{setting}=#{name}", true)
164
167
 
165
- Rails.logger&.warn("[PermissionForge] SECURITY warning: '#{name}' is listed in config.#{setting} but is no " \
166
- 'model in this process: nothing is masked for it. It is tried again at the next request or job.')
168
+ Warnings.emit("masked_models.unknown_listed.#{setting}=#{name}",
169
+ "'#{name}' is listed in config.#{setting} but is no model in this process: nothing is " \
170
+ 'masked for it. It is tried again at the next request or job.', severity: :security)
167
171
  end
168
172
 
169
173
  # Every tenant's rows: a model is masked for the whole process.
@@ -187,29 +191,31 @@ module PermissionForge
187
191
  #
188
192
  # @return [String, nil] why the concern cannot be included now
189
193
  def unmaskable_reason(klass)
190
- return 'its table cannot be read' unless readable_table?(klass)
191
-
192
- 'an earlier attempt failed halfway; restart the process' if klass.include?(PermissionForge::Concerns::MaskedAttributes)
194
+ unreadable_table(klass) ||
195
+ ('an earlier attempt failed halfway; restart the process' if klass.include?(PermissionForge::Concerns::MaskedAttributes))
193
196
  end
194
197
 
195
- # The table exists and can be read. A table found missing is looked up
196
- # again next time (the cached answer is dropped): a migration may run.
197
- def readable_table?(klass)
198
- return true if klass.table_exists?
198
+ # Why the table cannot be read, nil when it exists and can be. A table
199
+ # found missing is looked up again next time (the cached answer is
200
+ # dropped): a migration may run. Its database may be another one than
201
+ # the gem's (a host on several databases); a check that raised names
202
+ # the error (DatabaseReach): the model's own table_name, its database
203
+ # not configured.
204
+ def unreadable_table(klass)
205
+ cause = nil
206
+ return if DatabaseReach.table_exists?(klass) { |error| cause = error }
199
207
 
200
- klass.reset_column_information
201
- false
202
- rescue ActiveRecord::StatementInvalid, ActiveRecord::ConnectionNotEstablished
203
- false
208
+ cause ? "its table cannot be read (#{cause.class}: #{cause.message.lines.first&.strip})" : 'its table cannot be read'
204
209
  end
205
210
 
206
- # Logs once per model and process (MaskingSync retries every request).
211
+ # Warns once per model and process (MaskingSync retries every request).
207
212
  def warn_unmaskable(klass, why)
208
213
  name = klass.to_s
209
214
  return if unmaskable_warned.put_if_absent(name, true)
210
215
 
211
- Rails.logger&.warn("[PermissionForge] SECURITY warning: #{name} has a Resource row but #{why}: its columns " \
212
- 'are NOT masked in this process. It is tried again at the next request or job.')
216
+ Warnings.emit("masked_models.unmaskable.#{name}",
217
+ "#{name} has a Resource row but #{why}: its columns are NOT masked in this process. " \
218
+ 'It is tried again at the next request or job.', severity: :security)
213
219
  end
214
220
 
215
221
  # The models (and unknown listed names) whose warning this process has logged.