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,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'securerandom'
4
+ require_relative 'database_reach'
4
5
 
5
6
  module PermissionForge
6
7
  # Carries auto-applied masking (MaskedModels) to every process. The
@@ -33,7 +34,17 @@ module PermissionForge
33
34
  # Either way a process that has not synced (its boot could not reach the
34
35
  # database, a model could not be masked) applies at its next executor
35
36
  # run, and a development code reload applies again (`to_prepare`).
36
- # The store must be shared (not :memory or :null).
37
+ #
38
+ # The token needs a shared store. One that keeps nothing (:null, a
39
+ # :null_store) is never asked for it: the token would read as lost on
40
+ # every check. One that lives in each process (:memory, a :memory_store)
41
+ # reaches no other process. In development, where Rails' default store is
42
+ # one of these, each executor run re-reads the Resource names from the
43
+ # database instead, at most every DATABASE_RECHECK seconds (database
44
+ # recheck): a model named from a console, `bin/rails runner` or a rake task
45
+ # is masked in the server at its next request. Other environments get no
46
+ # such query (CacheWarnings warns at boot): a process then masks a model
47
+ # first named in another one only after a restart.
37
48
  #
38
49
  # @api private
39
50
  module MaskingSync
@@ -45,6 +56,21 @@ module PermissionForge
45
56
  # Seconds the store is left alone after it did not answer.
46
57
  RETRY_PAUSE = 30
47
58
 
59
+ # Development with a store that cannot carry the token: seconds between
60
+ # two reads of the Resource names (one query each).
61
+ DATABASE_RECHECK = 2
62
+
63
+ # The cause named by the token warning when the store raised nothing: a
64
+ # read of the token, and a read right after writing one, came back empty.
65
+ SILENT_STORE = ' (it did not answer, or a read right after a write came back empty: the store is down, full, ' \
66
+ 'or evicts the key at once)'
67
+
68
+ # check!'s warning when a sync raised: a SECURITY one, the process fails open until the next sync.
69
+ SYNC_FAILED = 'Auto-applied masking could not be synced: a model named in another process may stay unmasked here until the next sync'
70
+ # The boot step's warning when a model could not be masked (boot!).
71
+ INCOMPLETE_AT_BOOT = 'Auto-applied masking is not complete at boot (see the warnings above, or the Resource ' \
72
+ 'table cannot be read): it is tried again at each request or job.'
73
+
48
74
  # One sync (or apply) at a time; one probe of a failing store at a time.
49
75
  SYNC_LOCK = Mutex.new
50
76
  PROBE_LOCK = Mutex.new
@@ -57,20 +83,34 @@ module PermissionForge
57
83
  @announcement_owed = false
58
84
  @failing_until = nil
59
85
  @failing_store = nil
86
+ @recheck_after = nil
60
87
 
61
88
  class << self
62
89
  # Executor `to_run`. Row-driven: one store read; applies when another
63
90
  # process named a model since this one last synced. The store is not
64
- # read while it is failing (RETRY_PAUSE), nor at all for lists: the
65
- # token is then unknown (nil), and only a process that has not synced
66
- # applies (from the database). Never raises.
91
+ # read while it is failing (RETRY_PAUSE), nor at all for lists or a
92
+ # store that keeps nothing: the token is then unknown (nil), and only
93
+ # a process that has not synced applies (from the database). In
94
+ # development with a store that cannot carry the token, the Resource
95
+ # names instead (database_recheck?). Never raises.
96
+ #
97
+ # A process that has not synced (its boot could not use the database)
98
+ # asks the database first (database_unavailable?: one table check per
99
+ # run until it syncs, none after): while it cannot
100
+ # read the Resource table, nothing can be masked from it, and the store
101
+ # is not read — its own database may be the one that is not there
102
+ # (Solid Cache in a `rails new` app whose production database is not
103
+ # configured raised the same ArgumentError, logged as a SECURITY store
104
+ # failure on every runner).
67
105
  def check!
68
106
  return unless MaskedModels.enabled?
107
+ return recheck_database if database_recheck?
108
+ return if database_unavailable?
69
109
 
70
- token = MaskedModels.row_driven? ? answered_token : nil
110
+ token = token_in_store? ? answered_token : nil
71
111
  sync(token) if changed?(token)
72
112
  rescue StandardError => e
73
- Rails.logger&.error("[PermissionForge] auto-applied masking could not be synced — #{e.class}: #{e.message}")
113
+ Warnings.emit(:masking_sync_failed, "#{SYNC_FAILED} (#{e.class}: #{e.message}).", severity: :security)
74
114
  end
75
115
 
76
116
  # After a Resource row naming `name` has been created or renamed and
@@ -85,9 +125,9 @@ module PermissionForge
85
125
  # (asked only when it decides something)
86
126
  def named!(name)
87
127
  return unless MaskedModels.enabled?
88
- return apply!(name) unless MaskedModels.row_driven?
128
+ return apply!(name) unless token_in_store?
89
129
 
90
- masked_here?(name) && yield ? apply!(name) : announce!(name)
130
+ MaskedModels.masked?(name) && yield ? apply!(name) : announce!(name)
91
131
  end
92
132
 
93
133
  # The boot step: applies to every selected model and syncs with the
@@ -98,11 +138,8 @@ module PermissionForge
98
138
  #
99
139
  # @return [Boolean] whether every selected model is masked
100
140
  def boot!
101
- done = sync(MaskedModels.row_driven? ? current_token : nil, always: true)
102
- unless done
103
- Rails.logger&.warn('[PermissionForge] Auto-applied masking is not complete at boot (see the warnings ' \
104
- 'above, or the Resource table cannot be read): it is tried again at each request or job.')
105
- end
141
+ done = sync(token_in_store? ? current_token : nil, always: true)
142
+ Warnings.emit(:masking_sync_incomplete_at_boot, INCOMPLETE_AT_BOOT, severity: :security) unless done
106
143
  done
107
144
  end
108
145
 
@@ -123,6 +160,7 @@ module PermissionForge
123
160
  SYNC_LOCK.synchronize { @token = UNSYNCED }
124
161
  @announcement_owed = false
125
162
  @failing_until = nil
163
+ @recheck_after = nil
126
164
  MaskedModels.reset_warnings!
127
165
  end
128
166
 
@@ -138,8 +176,43 @@ module PermissionForge
138
176
  !until_time.nil? && now < until_time && PermissionForge.cache.equal?(@failing_store)
139
177
  end
140
178
 
179
+ # Whether this process learns of a model named in another one from the
180
+ # Resource rows (development, a row-driven setting, a store that cannot
181
+ # carry the token: it keeps nothing or lives in each process). Asks
182
+ # nothing outside development.
183
+ #
184
+ # @return [Boolean]
185
+ def database_recheck?
186
+ return false unless Rails.env.development? && MaskedModels.row_driven?
187
+
188
+ cache = PermissionForge.cache
189
+ cache.process_local? || !cache.keeps_entries?
190
+ end
191
+
141
192
  private
142
193
 
194
+ # A process that has not synced, whose Resource table cannot be read
195
+ # (DatabaseReach: unreachable, not configured, not migrated).
196
+ def database_unavailable? = !synced? && !DatabaseReach.table_exists?(PermissionForge::Resource)
197
+
198
+ # Whether the token is read and written: a row-driven setting and a
199
+ # store that keeps entries, outside a database recheck.
200
+ #
201
+ # @return [Boolean]
202
+ def token_in_store?
203
+ MaskedModels.row_driven? && PermissionForge.cache.keeps_entries? && !database_recheck?
204
+ end
205
+
206
+ # The database recheck: every model the Resource rows name, at most
207
+ # every DATABASE_RECHECK seconds — and at each run until it succeeded
208
+ # once (a process that has not synced).
209
+ def recheck_database
210
+ return if synced? && @recheck_after && now < @recheck_after
211
+
212
+ @recheck_after = now + DATABASE_RECHECK
213
+ sync(nil, always: true)
214
+ end
215
+
143
216
  # Applies the concern to one model in this process.
144
217
  def apply!(name)
145
218
  SYNC_LOCK.synchronize { MaskedModels.apply(only: name) }
@@ -157,8 +230,9 @@ module PermissionForge
157
230
  apply!(name)
158
231
  return if publish_token
159
232
 
160
- Rails.logger&.warn('[PermissionForge] The cache store did not take the masking token: other processes ' \
161
- "mask #{name} once the store answers again and this process has written it.")
233
+ Warnings.emit("masking_sync.token_not_taken.#{name}",
234
+ "The cache store did not take the masking token: other processes mask #{name} once the " \
235
+ 'store answers again and this process has written it.', severity: :security)
162
236
  end
163
237
 
164
238
  # The token to check against — nil while the store is failing. A
@@ -228,16 +302,23 @@ module PermissionForge
228
302
  @failing_until = nil
229
303
  end
230
304
 
231
- # Logged once per outage: the first failure after the store answered.
305
+ # Warned once per outage: the first failure after the store answered.
306
+ # The warning names the cause (the error, or what a silent store did)
307
+ # and the remedy.
232
308
  def store_failed!(error = nil)
233
309
  first = @failing_until.nil?
234
310
  @failing_until = now + RETRY_PAUSE
235
311
  @failing_store = PermissionForge.cache
236
312
  return unless first
237
313
 
238
- why = error ? " (#{error.class}: #{error.message})" : ''
239
- Rails.logger&.warn("[PermissionForge] The cache store did not give or keep the masking token#{why}: auto-applied " \
240
- "masking is not synced with other processes for the next #{RETRY_PAUSE} seconds.")
314
+ Warnings.emit(:masking_sync_store_failed,
315
+ 'The cache store did not give or keep the masking token' \
316
+ "#{error ? " (#{error.class}: #{error.message})" : SILENT_STORE}: auto-applied " \
317
+ "masking is not synced with other processes for the next #{RETRY_PAUSE} seconds — a model " \
318
+ 'a Resource row names in another process is masked here once the store answers. A store ' \
319
+ 'that is up but loses the token at once cannot sync masking: use a shared store that keeps ' \
320
+ 'entries, or include PermissionForge::Concerns::MaskedAttributes in the models ' \
321
+ '(docs/masked-attributes.md).', severity: :security)
241
322
  end
242
323
 
243
324
  def now
@@ -251,12 +332,6 @@ module PermissionForge
251
332
  token.nil? ? !synced? : token != @token
252
333
  end
253
334
 
254
- # Whether this process has the concern on the model (false for a name
255
- # that resolves to no model here: not known to be masked).
256
- def masked_here?(name)
257
- MaskedModels.masked?(name)
258
- end
259
-
260
335
  def sync(token, always: false)
261
336
  SYNC_LOCK.synchronize do
262
337
  return true if !always && synced? && token == @token
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'warnings'
4
+
3
5
  module PermissionForge
4
6
  # What a model took from the configuration as it loaded — the user class of
5
7
  # an assignment (PermissionSetUser#user, PermissionSetGroupUser#user), the
@@ -8,16 +10,21 @@ module PermissionForge
8
10
  # changed after the class loaded (a PermissionForge.configure or a setter
9
11
  # in after_initialize, a console) changes nothing in the class: a
10
12
  # development reload picks it up, an eager-loaded production process never
11
- # does. The engine logs each difference once the host's after_initialize
12
- # blocks have run, and PermissionForge.configure logs it too. Nothing here
13
- # loads a model. (table_name_prefix is not compared: table_name is a
14
- # method that reads it on every call.)
13
+ # does. The engine warns about each difference (Warnings.emit) once the
14
+ # host's after_initialize blocks have run, and PermissionForge.configure
15
+ # does too. Nothing here loads a model. (table_name_prefix is not
16
+ # compared: table_name is a method that reads it on every call.)
15
17
  #
16
18
  # @api private
17
19
  module ModelBindings
18
20
  # The settings a model declares as it loads, each with the association
19
21
  # that carries it (the tenant is read from HasTenantScoping).
20
22
  ASSOCIATIONS = { user_class: :user, audit_user_class: :created_by }.freeze
23
+ # The severity of a mismatch, by setting (Warnings): a model loaded with
24
+ # another tenant model (or none) is not isolated as configured, access
25
+ # across tenants; another user class only points an association
26
+ # elsewhere.
27
+ SEVERITY = { tenant_model: :security }.freeze
21
28
 
22
29
  class << self
23
30
  # One sentence per setting that loaded models declared differently.
@@ -25,22 +32,18 @@ module PermissionForge
25
32
  # @param config [Configuration]
26
33
  # @return [Array<String>]
27
34
  def mismatches(config = PermissionForge.configuration)
28
- found = Hash.new { |hash, key| hash[key] = [] }
29
- loaded_models.each do |model|
30
- declared_otherwise(model, config).each do |setting, declared|
31
- found[[setting, config.public_send(setting), declared]] << model.name
32
- end
33
- end
34
- found.map { |difference, models| sentence(difference, models) }
35
+ differences(config).map { |difference, models| sentence(difference, models) }
35
36
  end
36
37
 
37
- # Logs every mismatch.
38
+ # Warns about every mismatch, one key per setting.
38
39
  #
39
40
  # @param config [Configuration]
40
- # @param logger [Logger, nil]
41
41
  # @return [void]
42
- def warn!(config = PermissionForge.configuration, logger: default_logger)
43
- mismatches(config).each { |sentence| logger&.warn("[PermissionForge] #{sentence}") }
42
+ def warn!(config = PermissionForge.configuration)
43
+ differences(config).each do |difference, models|
44
+ setting = difference.first
45
+ Warnings.emit("model_bindings.#{setting}", sentence(difference, models), severity: SEVERITY.fetch(setting, :config))
46
+ end
44
47
  end
45
48
 
46
49
  # The model classes already loaded, without loading one.
@@ -53,6 +56,18 @@ module PermissionForge
53
56
 
54
57
  private
55
58
 
59
+ # { [setting, what the configuration says, what the models declared] =>
60
+ # the model names } for every difference.
61
+ def differences(config)
62
+ found = Hash.new { |hash, key| hash[key] = [] }
63
+ loaded_models.each do |model|
64
+ declared_otherwise(model, config).each do |setting, declared|
65
+ found[[setting, config.public_send(setting), declared]] << model.name
66
+ end
67
+ end
68
+ found
69
+ end
70
+
56
71
  # [setting, what the model declared] for each setting the model
57
72
  # declared and the configuration now says differently. A model without
58
73
  # the association (a PermissionSet has no :user) declares nothing.
@@ -81,10 +96,6 @@ module PermissionForge
81
96
  'development; never in an eager-loaded production process). Configure PermissionForge in ' \
82
97
  'config/initializers.'
83
98
  end
84
-
85
- def default_logger
86
- Rails.logger if defined?(Rails) && Rails.respond_to?(:logger)
87
- end
88
99
  end
89
100
  end
90
101
  end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ # "This name is a model with a table" has one definition: the class a
5
+ # Resource name or a rule's Resource stands for, when it is an ActiveRecord
6
+ # model that is not abstract and whose table exists. Anything else — a
7
+ # module (`Kernel`), a class that is no model (`String`), an abstract
8
+ # model, a model whose table is not there yet, a name that is no constant,
9
+ # a constant whose file raises while it loads — is no model: the callers
10
+ # describe or skip, they never fail on it (a Resource named `Kernel` made
11
+ # the field generator raise NoMethodError and the API answer 500). A table
12
+ # found missing is asked of the database again once (SchemaRead.table?).
13
+ # The one place a Resource name is constantized (a G4 needle).
14
+ #
15
+ # @example
16
+ # ModelLookup.model_for('Customer') # => Customer
17
+ # ModelLookup.model_for('Kernel') # => nil
18
+ #
19
+ # @api private
20
+ module ModelLookup
21
+ # What is rescued while a name is resolved: anything raised as the
22
+ # constant loads, a model file's SyntaxError or LoadError included.
23
+ UNTELLABLE = [StandardError, ScriptError].freeze
24
+
25
+ module_function
26
+
27
+ # @param name [String, Symbol, nil] a constant path
28
+ # @return [Class, nil] the model, or nil when the name is no model with a table
29
+ def model_for(name)
30
+ klass = name.to_s.safe_constantize
31
+ klass if klass.is_a?(Class) && klass < ActiveRecord::Base && !klass.abstract_class? && SchemaRead.table?(klass, klass.table_name)
32
+ rescue *UNTELLABLE
33
+ nil
34
+ end
35
+
36
+ # @param name [String, Symbol, nil]
37
+ # @return [Boolean] whether the name is a model with a table
38
+ def model?(name)
39
+ !model_for(name).nil?
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Operators
5
+ # The model whose encrypted column a rule names through a name its
6
+ # query gives a table that is on no model along the rule's path: an
7
+ # alias (`twins_notes.body` on a self-join), a `has_many :through`'s
8
+ # join table, a table a default scope or a host's join brings in
9
+ # (RuleSources). RuleColumn leaves such a name to the database
10
+ # (UNKNOWN) — except a column that holds ciphertext, which no built-in
11
+ # operator can compare (RuleColumn#encrypted?): failing to see one would
12
+ # compare the ciphertext, and `not_equal` would show the rows it should
13
+ # hide.
14
+ #
15
+ # - The name is resolved to the table it stands for, and every loaded
16
+ # model on that table (PermissionForge::EncryptedAttributes.models_on,
17
+ # single-table inheritance included) says whether it encrypts the
18
+ # column: one that does makes it encrypted.
19
+ # - A name whose table cannot be told, or that no loaded model is on (a
20
+ # subquery, a table without a model), is judged by every model along
21
+ # the path, the rule's own included: a column of that name that any of
22
+ # them encrypts counts as encrypted (fail closed).
23
+ #
24
+ # Names are compared as the database compares them (RuleColumn.same_name?).
25
+ #
26
+ # @api private
27
+ class EncryptedAlias
28
+ # @param models [Array<Class>] the rule's model, then the model of each
29
+ # association on its path
30
+ # @param joined [Hash{String => String, nil}, Array<String>, nil] the
31
+ # names the rule's query gives its tables (RuleSources.tables; a list
32
+ # of names: their tables not known)
33
+ def initialize(models, joined)
34
+ @models = models
35
+ @joined = joined || {}
36
+ @exact = RuleColumn.exact_names?(models.last)
37
+ end
38
+
39
+ # The model whose encrypted column `name` is, when `table` is a name
40
+ # the query gives a table: one on that table, or one along the path;
41
+ # nil otherwise.
42
+ #
43
+ # @param table [String] the field name's qualifier
44
+ # @param name [String] the column as written
45
+ # @return [Class, nil]
46
+ def model(table, name)
47
+ source = @joined.find { |alias_name, _table| RuleColumn.same_name?(alias_name, table, @exact) } or return
48
+
49
+ (on_table(source).presence || @models).find { |model| encrypts?(model, name) }
50
+ end
51
+
52
+ private
53
+
54
+ # The loaded models on the table the name stands for.
55
+ def on_table(source)
56
+ _alias_name, table = source
57
+ return [] unless table
58
+
59
+ PermissionForge::EncryptedAttributes.models_on(table, exact: @exact)
60
+ end
61
+
62
+ def encrypts?(model, name)
63
+ PermissionForge::EncryptedAttributes.names(model).any? { |attribute| RuleColumn.same_name?(attribute.to_s, name, @exact) }
64
+ end
65
+ end
66
+ end
67
+ end
@@ -18,6 +18,12 @@ module PermissionForge
18
18
  #
19
19
  # @api public
20
20
  module Registry
21
+ # The operators whose strategy adds the NULL rows under a rule's
22
+ # `include_nulls` ("…, or empty"); the others ignore the flag. The one
23
+ # list: the RLS policy (Services::RlsOperatorSql), the rule builder and
24
+ # the rule's sentence read it (a test compares it with the strategies).
25
+ NULL_INCLUDING = [Constants::EQUAL, Constants::NOT_EQUAL, Constants::IN, Constants::NOT_IN].freeze
26
+
21
27
  module_function
22
28
 
23
29
  # @param name [String] operator constant from PermissionSetRule (e.g., 'equal')