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
@@ -4,19 +4,48 @@ module PermissionForge
4
4
  # @api private
5
5
  module Api
6
6
  module Serializers
7
+ # The JSON API's built-in serializers: `attributes` lists the keys of
8
+ # an answer, in order; each is the serializer's own (private) method
9
+ # of that name, else the record's.
10
+ #
11
+ # A key that refers to another PermissionForge record is declared with
12
+ # {.reference}: the record's numeric id (`permission_set_id`, as
13
+ # through 0.14.1) followed by its uuid (`permission_set_uuid`), so a
14
+ # client that only knows uuids — `api_record_identifier = :uuid`, the
15
+ # identifier the URLs take — can follow it (field test round 8, D#29).
16
+ # Both keys are always there, whatever the identifier setting. The uuid
17
+ # key is answer-only: no endpoint takes it (the `_id` key changes a
18
+ # reference, and takes the uuid under `:uuid`), so a whole answer sent
19
+ # back is ignored there. A collection looks the uuids up in one query
20
+ # per reference.
7
21
  class BaseSerializer
8
22
  def initialize(record)
9
23
  @record = record
10
24
  end
11
25
 
26
+ # @return [Hash{Symbol => Object}] the record's keys, in order
12
27
  def as_json(*)
13
28
  self.class.attribute_names.to_h do |attr|
14
29
  [attr, respond_to?(attr, true) ? send(attr) : @record.public_send(attr)]
15
30
  end
16
31
  end
17
32
 
33
+ # The uuids of the records the references name, as {.reference_uuids}
34
+ # answers them (a collection passes the lookup of all its records).
35
+ #
36
+ # @param uuids [Hash{Symbol => Hash{Integer => String}}]
37
+ # @return [self]
38
+ def with_reference_uuids(uuids)
39
+ @reference_uuids = uuids
40
+ self
41
+ end
42
+
43
+ # @param records [Enumerable<ActiveRecord::Base>]
44
+ # @return [Array<Hash>]
18
45
  def self.collection(records)
19
- records.map { |r| new(r).as_json }
46
+ records = records.to_a
47
+ uuids = reference_uuids(records)
48
+ records.map { |r| new(r).with_reference_uuids(uuids).as_json }
20
49
  end
21
50
 
22
51
  def self.attributes(*attrs)
@@ -27,9 +56,66 @@ module PermissionForge
27
56
  @attribute_names || []
28
57
  end
29
58
 
59
+ # The two keys of a reference to another PermissionForge record, for
60
+ # `attributes`: `<association>_id` (its numeric id) and
61
+ # `<association>_uuid` (its uuid, read through the association's
62
+ # model; nil when the id is nil or names no record).
63
+ #
64
+ # @param association [Symbol] a belongs_to of the serialized model
65
+ # @return [Array(Symbol, Symbol)]
66
+ def self.reference(association)
67
+ id_key, uuid_key = reference_keys(association)
68
+ own_references << association
69
+ define_method(uuid_key) { reference_uuid(association, id_key) }
70
+ private uuid_key
71
+ [id_key, uuid_key]
72
+ end
73
+
74
+ # The one naming of a reference's keys: the id key and the uuid key.
75
+ #
76
+ # @param association [Symbol, String]
77
+ # @return [Array(Symbol, Symbol)]
78
+ def self.reference_keys(association)
79
+ [:"#{association}_id", :"#{association}_uuid"]
80
+ end
81
+
82
+ # @return [Array<Symbol>] the associations declared with {.reference}, a superclass's too
83
+ def self.references
84
+ inherited = superclass.respond_to?(:references) ? superclass.references : []
85
+ inherited | own_references
86
+ end
87
+
88
+ # The uuid of every record the references of `records` name, one
89
+ # query per reference (the referenced model's default scope, the
90
+ # tenant's, applies as it does to the association).
91
+ #
92
+ # @param records [Array<ActiveRecord::Base>]
93
+ # @return [Hash{Symbol => Hash{Integer => String}}] association => { id => uuid }
94
+ def self.reference_uuids(records)
95
+ references.to_h { |association| [association, uuids_named(records, association)] }
96
+ end
97
+
98
+ def self.own_references
99
+ @own_references ||= []
100
+ end
101
+
102
+ def self.uuids_named(records, association)
103
+ id_key, = reference_keys(association)
104
+ ids = records.filter_map { |record| record.public_send(id_key) }.uniq
105
+ return {} if ids.empty?
106
+
107
+ records.first.class.reflect_on_association(association).klass.where(id: ids).pluck(:id, :uuid).to_h
108
+ end
109
+ private_class_method :own_references, :uuids_named
110
+
30
111
  private
31
112
 
32
113
  attr_reader :record
114
+
115
+ def reference_uuid(association, id_key)
116
+ @reference_uuids ||= self.class.reference_uuids([record])
117
+ @reference_uuids.fetch(association, {})[record.public_send(id_key)]
118
+ end
33
119
  end
34
120
  end
35
121
  end
@@ -4,7 +4,7 @@ module PermissionForge
4
4
  module Api
5
5
  module Serializers
6
6
  class PermissionSetGroupUserSerializer < BaseSerializer
7
- attributes :id, :uuid, :permission_set_group_id, :user_id, :created_at, :updated_at
7
+ attributes :id, :uuid, *reference(:permission_set_group), :user_id, :created_at, :updated_at
8
8
  end
9
9
  end
10
10
  end
@@ -6,7 +6,7 @@ module PermissionForge
6
6
  class PermissionSetRuleSerializer < BaseSerializer
7
7
  REDACTED_PLACEHOLDER = '[REDACTED]'
8
8
 
9
- attributes :id, :uuid, :permission_set_id, :resource_id,
9
+ attributes :id, :uuid, *reference(:permission_set), *reference(:resource),
10
10
  :name, :failure_message, :join_type,
11
11
  :field_name, :field_value, :field_accessor_path,
12
12
  :operator, :include_nulls, :is_active, :is_editable,
@@ -3,10 +3,13 @@
3
3
  module PermissionForge
4
4
  module Api
5
5
  module Serializers
6
+ # `allowed_ips` is the stored text (commas between the entries since
7
+ # 0.15); `allowed_ip_list` the entries as the engine reads them
8
+ # (ActivationConditions) — what a client should write back.
6
9
  class PermissionSetSerializer < BaseSerializer
7
10
  attributes :id, :uuid, :name, :description, :is_active, :is_editable, :is_muting,
8
- :requires_session_flag, :allowed_ips, :allowed_hours_start, :allowed_hours_end,
9
- :permission_set_group_id, :created_at, :updated_at
11
+ :requires_session_flag, :allowed_ips, :allowed_ip_list, :allowed_hours_start, :allowed_hours_end,
12
+ *reference(:permission_set_group), :created_at, :updated_at
10
13
  end
11
14
  end
12
15
  end
@@ -4,7 +4,7 @@ module PermissionForge
4
4
  module Api
5
5
  module Serializers
6
6
  class PermissionSetUserSerializer < BaseSerializer
7
- attributes :id, :uuid, :permission_set_id, :user_id, :expires_at, :created_at, :updated_at
7
+ attributes :id, :uuid, *reference(:permission_set), :user_id, :expires_at, :created_at, :updated_at
8
8
  end
9
9
  end
10
10
  end
@@ -4,7 +4,7 @@ module PermissionForge
4
4
  module Api
5
5
  module Serializers
6
6
  class ResourceActionSerializer < BaseSerializer
7
- attributes :id, :uuid, :resource_id, :name, :label, :description,
7
+ attributes :id, :uuid, *reference(:resource), :name, :label, :description,
8
8
  :is_permitted, :is_active,
9
9
  :created_at, :updated_at
10
10
  end
@@ -4,7 +4,7 @@ module PermissionForge
4
4
  module Api
5
5
  module Serializers
6
6
  class ResourceFieldSerializer < BaseSerializer
7
- attributes :id, :uuid, :resource_id, :name, :label, :description,
7
+ attributes :id, :uuid, *reference(:resource), :name, :label, :description,
8
8
  :is_active, :is_editable,
9
9
  :read_access, :edit_access, :mask_access, :mask_strategy,
10
10
  :created_at, :updated_at
@@ -4,7 +4,7 @@ module PermissionForge
4
4
  module Api
5
5
  module Serializers
6
6
  class ResourceSerializer < BaseSerializer
7
- attributes :id, :uuid, :permission_set_id, :name, :description,
7
+ attributes :id, :uuid, *reference(:permission_set), :name, :description,
8
8
  :is_active, :is_editable,
9
9
  :read_access, :create_access, :edit_access, :delete_access,
10
10
  :view_all, :modify_all,
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ # The JSON API's authorization lambdas, and what the API refuses while one
5
+ # it needs is unset. `api_auth` proves identity only: while
6
+ # api_require_authorization is on (the default) a change needs
7
+ # api_action_authorization and an assignment api_assignment_authorization,
8
+ # and while api_stats_require_admin is on (the default) GET /api/stats
9
+ # needs api_stats_authorization. Without one, the API answers 403 and the
10
+ # answer names the option: it fails closed, but nothing said so before the
11
+ # first refused request (field test round 8, D#7). The boot note
12
+ # (BootChecks, :api_authorization_unset) and the audit finding
13
+ # (ConfigCheck#api_authorization_unset) both read `unset`.
14
+ #
15
+ # The stats note cannot see an api_auth that still sets the deprecated
16
+ # 0.14 `@admin = true` on the controller (Api::DashboardController
17
+ # #legacy_admin_flag?, until 0.16): that host is not refused, and the note
18
+ # says so.
19
+ #
20
+ # api_require_authorization = false with a lambda unset is another
21
+ # condition (every caller api_auth admits may change permissions: the
22
+ # api_authorization_gap warning, the api_mutations_unguarded finding).
23
+ #
24
+ # @api private
25
+ module ApiAuthorizations
26
+ # Each lambda => [the setting under which the API needs it, what the API
27
+ # refuses (403) while it is unset].
28
+ LAMBDAS = {
29
+ api_action_authorization: [:api_require_authorization, 'every create, update, delete and copy'],
30
+ api_assignment_authorization: [:api_require_authorization, 'every assignment and group membership change'],
31
+ api_stats_authorization: [:api_stats_require_admin,
32
+ 'GET /api/stats (or set api_stats_require_admin = false; an api_auth that sets the ' \
33
+ 'deprecated @admin = true still passes until 0.16)']
34
+ }.freeze
35
+
36
+ module_function
37
+
38
+ # The lambdas the API needs and does not have. None while the API is off
39
+ # or api_auth is unset (every request is then a 401).
40
+ #
41
+ # @param config [Configuration]
42
+ # @return [Hash{Symbol => String}] lambda option => what the API refuses without it
43
+ def unset(config)
44
+ return {} unless config.api_enabled && config.api_auth
45
+
46
+ LAMBDAS.each_with_object({}) do |(option, (required_by, refused)), unset|
47
+ unset[option] = refused if config.public_send(required_by) && !config.public_send(option)
48
+ end
49
+ end
50
+
51
+ # One line per unset lambda: "<option>: <what is refused>".
52
+ #
53
+ # @param config [Configuration]
54
+ # @return [Array<String>]
55
+ def lines(config)
56
+ unset(config).map { |option, refused| "#{option}: #{refused}" }
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ # The admin screens in an API-only application (`rails new --api`,
5
+ # `config.api_only = true`). Rails then leaves ActionDispatch::Cookies, the
6
+ # session store and ActionDispatch::Flash out of the middleware stack, and
7
+ # every admin page that renders a form raises: NameError for `flash` (its
8
+ # request methods are loaded with the middleware), then
9
+ # ActionDispatch::Request::Session::DisabledSessionError for the CSRF
10
+ # token. The JSON API needs none of them. BootChecks warns through
11
+ # Warnings.emit (:api_only_admin), naming the middlewares
12
+ # config/application.rb must add, when the engine is mounted — once the
13
+ # routes are drawn (the engine's config.after_routes_loaded), and not when
14
+ # config.api_only_admin_warning is false (an app that uses only the API).
15
+ #
16
+ # @api private
17
+ module ApiOnlyAdmin
18
+ COOKIES = 'ActionDispatch::Cookies'
19
+ SESSION = 'ActionDispatch::Session::CookieStore'
20
+ FLASH = 'ActionDispatch::Flash'
21
+ # What each one is recognised by in the stack: any session store counts.
22
+ PRESENT = {
23
+ COOKIES => ->(name) { name == COOKIES },
24
+ SESSION => ->(name) { name.start_with?('ActionDispatch::Session::') },
25
+ FLASH => ->(name) { name == FLASH }
26
+ }.freeze
27
+
28
+ module_function
29
+
30
+ # @param application [Rails::Application]
31
+ # @return [Array<String>] the middlewares the admin needs that the stack
32
+ # lacks; empty unless the application is api_only with the engine mounted
33
+ def missing_middlewares(application)
34
+ return [] unless api_only?(application) && mounted?(application)
35
+
36
+ names = middleware_names(application)
37
+ return [] if names.nil?
38
+
39
+ PRESENT.reject { |_name, present| names.any?(&present) }.keys
40
+ end
41
+
42
+ def api_only?(application)
43
+ application.respond_to?(:config) && application.config.respond_to?(:api_only) && application.config.api_only == true
44
+ end
45
+
46
+ # @param missing [Array<String>]
47
+ # @return [String] the warning
48
+ def message(missing)
49
+ 'The application is API-only (config.api_only = true) and PermissionForge::Engine is mounted: the admin screens ' \
50
+ "need #{missing.join(', ')} in the middleware stack, which an API-only application leaves out. Every admin page " \
51
+ "that renders a form answers 500 until config/application.rb adds #{missing.map { |name| "config.middleware.use #{name}" }.join(', ')} " \
52
+ '(docs/admin-ui.md#api-only-applications). The JSON API works without them.'
53
+ end
54
+
55
+ # The names in the built stack (after initialize!); nil while it is still
56
+ # the configuration proxy, when nothing can be said yet.
57
+ def middleware_names(application)
58
+ stack = application.middleware
59
+ return nil unless stack.respond_to?(:middlewares)
60
+
61
+ stack.middlewares.map { |middleware| middleware.name.to_s }
62
+ rescue StandardError
63
+ nil
64
+ end
65
+
66
+ # Whether the engine is mounted in the routes as drawn. Asked once they
67
+ # are (after_routes_loaded: at boot, or at their first use where Rails 8
68
+ # draws them lazily); it never draws them itself — a draw at boot is
69
+ # undone and drawn again at the first request. When they cannot be read,
70
+ # yes (a warning too many beats a 500 nobody was told about).
71
+ def mounted?(application)
72
+ application.routes.routes.any? { |route| engine?(route.app) }
73
+ rescue StandardError
74
+ true
75
+ end
76
+
77
+ def engine?(app)
78
+ app == PermissionForge::Engine || (app.respond_to?(:app) && app.app == PermissionForge::Engine)
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Audit
5
+ # The actor labels the gem writes on an event itself, stored as codes
6
+ # (never English), each for one actor type: the admin names them from
7
+ # the locale (`permission_forge.audit_log.actor_codes.<code>`,
8
+ # Admin::ActorLabel). A label a person or the host chose is shown as
9
+ # stored; so is a code under another actor type (a person called "boot").
10
+ #
11
+ # @api private
12
+ module ActorCodes
13
+ # An API request nobody named (api_auth said yes, but not who): ActorResolver's fallback.
14
+ API_CLIENT = 'api_client'
15
+ # The settings observer, which records the audit settings as the application boots.
16
+ BOOT = 'boot'
17
+ # actor type => the codes it carries.
18
+ BY_TYPE = { 'api' => [API_CLIENT], 'system' => [BOOT] }.freeze
19
+ # A label stored before it was a code => the code.
20
+ LEGACY = { 'API client' => API_CLIENT }.freeze
21
+
22
+ # @param type [String, Symbol] the event's actor_type
23
+ # @param label [String, nil] the event's actor_label
24
+ # @return [String, nil] the code the label stands for, nil for any other label
25
+ def self.code(type, label)
26
+ code = label.to_s.strip
27
+ code = LEGACY.fetch(code, code)
28
+ code if BY_TYPE.fetch(type.to_s, []).include?(code)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -25,7 +25,6 @@ module PermissionForge
25
25
  # username it has just verified. Not reachable from a header (those are
26
26
  # HTTP_*).
27
27
  BASIC_AUTH_ENV = 'permission_forge.audit.basic_auth_user'
28
- API_CLIENT_LABEL = 'API client'
29
28
 
30
29
  # The steps tried, in order, per request source; the first that names
31
30
  # someone wins.
@@ -37,7 +36,7 @@ module PermissionForge
37
36
  # Who acts when no step names anyone.
38
37
  FALLBACKS = {
39
38
  'admin_ui' => -> { Actor.unknown },
40
- 'api' => -> { Actor.new(type: 'api', label: API_CLIENT_LABEL) }
39
+ 'api' => -> { Actor.new(type: 'api', label: ActorCodes::API_CLIENT) }
41
40
  }.freeze
42
41
 
43
42
  # @param controller [ActionController::Metal]
@@ -5,23 +5,34 @@ module PermissionForge
5
5
  # The audit boot warnings, written by the engine once the database is
6
6
  # reachable. The missing-table warning is logged by Audit (once per
7
7
  # process, by Audit.verify_table!) with the text of .missing_table; this
8
- # adds auditing switched off and one shared login for every change.
8
+ # adds auditing switched off and one shared login for every change, each
9
+ # through PermissionForge::Warnings.emit.
10
+ #
11
+ # The table is not checked while a generator runs (`bin/rails generate
12
+ # permission_forge:install` boots the app before it writes the migration
13
+ # that creates the table): it is checked again, and the warning logged,
14
+ # the first time an event is recorded. The emitter writes nothing then.
9
15
  #
10
16
  # @api private
11
17
  module BootWarnings
12
- def self.call(configuration = PermissionForge.configuration)
13
- PermissionForge::Audit.verify_table!
18
+ # @param generating [Boolean] whether a Rails generator is running
19
+ def self.call(configuration = PermissionForge.configuration, generating: generator_running?)
20
+ PermissionForge::Audit.verify_table! unless generating
14
21
 
15
22
  if configuration.audit_events == false
16
- Rails.logger&.warn '[PermissionForge] SECURITY warning: audit_events is false, so permission changes ' \
17
- 'are not recorded. Set config.audit_events = :auto unless another system records them.'
23
+ PermissionForge::Warnings.emit(:audit_events_off,
24
+ 'audit_events is false, so permission changes are not recorded. Set ' \
25
+ 'config.audit_events = :auto unless another system records them.',
26
+ severity: :security)
18
27
  end
19
28
 
20
29
  return unless configuration.http_basic_auth_enabled && configuration.audit_actor_resolver.nil?
21
30
 
22
- Rails.logger&.warn '[PermissionForge] The admin UI uses HTTP Basic auth and audit_actor_resolver is not ' \
23
- 'set: every change is attributed to the shared login. Set ' \
24
- 'config.audit_actor_resolver to return the signed-in admin.'
31
+ PermissionForge::Warnings.emit(:audit_actor_shared_login,
32
+ 'The admin UI uses HTTP Basic auth and audit_actor_resolver is not set: ' \
33
+ 'every change is attributed to the shared login. Set ' \
34
+ 'config.audit_actor_resolver to return the signed-in admin.',
35
+ audience: :server)
25
36
  end
26
37
 
27
38
  # The missing-table warning (Audit logs it once per process). Its
@@ -39,10 +50,19 @@ module PermissionForge
39
50
  else
40
51
  'its migration is generated and run (bin/rails generate permission_forge:upgrade, then bin/rails db:migrate)'
41
52
  end
42
- "[PermissionForge] audit_events is :auto but the table '#{table}' was not found: no audit events are " \
53
+ "audit_events is :auto but the table '#{table}' was not found: no audit events are " \
43
54
  "recorded until #{remedy}. Changes still work."
44
55
  end
45
56
 
57
+ # `bin/rails generate` or `bin/rails destroy` is booting the app
58
+ # (Warnings.generator_running?: its command class is loaded, not
59
+ # merely Rails::Generators, which a gem may load at boot).
60
+ #
61
+ # @return [Boolean]
62
+ def self.generator_running?
63
+ PermissionForge::Warnings.generator_running?
64
+ end
65
+
46
66
  # @return [Boolean] whether a permission_forge migration in the app's
47
67
  # migration paths creates the audit events table
48
68
  def self.audit_migration_present?
@@ -15,11 +15,17 @@ module PermissionForge
15
15
  class UnknownEventError < ArgumentError; end
16
16
 
17
17
  # `notification` is computed once: Audit.instrument asks for it on hot paths.
18
+ # `cache`: the event sums up an operation that invalidated, cleared or
19
+ # removed cached permissions, so its data carries the one Cache::Outcome
20
+ # (`cache_confirmed`, `process_local`, `unconfirmed`).
18
21
  #
19
22
  # @api private
20
- Entry = Struct.new(:name, :category, :runtime_group, :notification, keyword_init: true) do
23
+ Entry = Struct.new(:name, :category, :runtime_group, :notification, :cache, keyword_init: true) do
21
24
  # Runtime events are published by Audit.instrument and stored per group.
22
25
  def runtime? = category == 'access'
26
+
27
+ # Whether the event's data carries a Cache::Outcome.
28
+ def cache? = cache == true
23
29
  end
24
30
 
25
31
  CATEGORIES = %w[change admin system access].freeze
@@ -34,10 +40,13 @@ module PermissionForge
34
40
  # @param category [String] change, admin, system or access
35
41
  # @param runtime_group [Symbol, nil] required for access events, refused
36
42
  # for the others
43
+ # @param cache [Boolean] the event's data carries a Cache::Outcome (a
44
+ # cache-touching operation's summary; test/permission_forge/audit/cache_scope_events_test.rb
45
+ # runs every flagged event's operation)
37
46
  # @return [Entry]
38
- def register(name, category:, runtime_group: nil)
47
+ def register(name, category:, runtime_group: nil, cache: false)
39
48
  entry = Entry.new(name: name.to_s, category: category.to_s, runtime_group: runtime_group&.to_sym,
40
- notification: "#{name}.permission_forge").freeze
49
+ notification: "#{name}.permission_forge", cache: cache == true).freeze
41
50
  validate!(entry)
42
51
  REGISTRY_MUTEX.synchronize { @entries[entry.name] = entry }
43
52
  end
@@ -92,15 +101,16 @@ module PermissionForge
92
101
  group_membership.added group_membership.changed group_membership.removed field.sensitive_acknowledged]
93
102
  .each { |name| register(name, category: 'change') }
94
103
 
95
- %w[erasure.performed consent.withdrawn cleanup.expired_assignments cache.cleared audit.purged audit.settings_observed
96
- report.generated].each { |name| register(name, category: 'system') }
104
+ %w[erasure.performed consent.withdrawn cleanup.expired_assignments cache.cleared cache.settled]
105
+ .each { |name| register(name, category: 'system', cache: true) }
106
+ %w[audit.purged audit.settings_observed report.generated].each { |name| register(name, category: 'system') }
97
107
 
98
108
  %w[admin.effective_access_viewed audit.viewed audit.exported admin.auth_failed api.auth_failed admin.lock_refused]
99
109
  .each { |name| register(name, category: 'admin') }
100
110
 
101
111
  {
102
112
  'access.denied' => :denied, 'action.denied' => :denied,
103
- 'rule.bypassed' => :bypass, 'query.unscoped' => :bypass,
113
+ 'rule.bypassed' => :bypass, 'query.unscoped' => :bypass, 'field.unmasked_without_user' => :bypass,
104
114
  'field.access' => :field_read,
105
115
  'params.filtered' => :params_filtered, 'write.masked_skipped' => :params_filtered,
106
116
  'permissions.rebuilt' => :cache, 'permissions.invalidated' => :cache
@@ -60,10 +60,10 @@ module PermissionForge
60
60
  problem(name, :invalid)
61
61
  end
62
62
 
63
- # Rails' own validation messages (errors.messages.<kind>), like the
64
- # 422 bodies of the other endpoints.
63
+ # The gem's own validation messages (permission_forge.validations.<kind>:
64
+ # blank, invalid), like the 422 bodies of the other endpoints.
65
65
  def problem(name, kind)
66
- @errors[name] << I18n.t("errors.messages.#{kind}")
66
+ @errors[name] << I18n.t("permission_forge.validations.#{kind}")
67
67
  end
68
68
 
69
69
  # A parameter's value as a String, nil when absent; an array or hash
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Audit
5
+ # The reasons the gem records on an event itself, stored as codes (never
6
+ # English): the admin renders a code from the locale
7
+ # (`permission_forge.audit_log.reason_codes.<code>`, helper
8
+ # `pf_audit_reason`) and any other reason as it was typed. The CSV export
9
+ # and the API keep the stored value.
10
+ #
11
+ # @api private
12
+ module ReasonCodes
13
+ # ExpiredAssignmentCleaner's reason when nobody gave one (rake, the admin's Clean up).
14
+ EXPIRED = 'expired'
15
+ # Every code.
16
+ ALL = [EXPIRED].freeze
17
+
18
+ # @param reason [String, nil] an event's (or a row's) stored reason
19
+ # @return [Boolean] whether it is one of the gem's codes
20
+ def self.code?(reason)
21
+ ALL.include?(reason.to_s.strip)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -102,9 +102,16 @@ module PermissionForge
102
102
  end
103
103
 
104
104
  # permissions.invalidated (the `cache` group): a user's cached hash
105
- # was removed, for which tenant, and whether the store confirmed it.
105
+ # was removed, for which tenant, whether the store confirmed it, and
106
+ # the scope of the store (Cache::Report: `process_local`, and
107
+ # `in_request` — which processes kept their entries when the store
108
+ # lives in each process).
106
109
  def invalidated(user, tenant_id:, confirmed:)
107
- cache_event('permissions.invalidated', user, 'tenant_id' => tenant_id, 'confirmed' => confirmed)
110
+ return unless Audit.instrument?('permissions.invalidated')
111
+
112
+ scope = PermissionForge::Cache::Report.new
113
+ cache_event('permissions.invalidated', user, 'tenant_id' => tenant_id, 'confirmed' => confirmed,
114
+ 'process_local' => scope.process_local?, 'in_request' => scope.in_request?)
108
115
  end
109
116
 
110
117
  # Whether this is the first time in this request that `key` was
@@ -27,12 +27,12 @@ module PermissionForge
27
27
  # @return [AuditEvent, nil] the new event; nil when nothing changed,
28
28
  # the table is missing, auditing is off or recording failed
29
29
  def call(configuration: PermissionForge.configuration)
30
- return unless PermissionForge::AuditEvent.table_exists?
30
+ return unless PermissionForge::DatabaseReach.table_exists?(PermissionForge::AuditEvent)
31
31
 
32
32
  settings = fingerprint(configuration)
33
33
  return if last_observed == settings
34
34
 
35
- payload = { data: { 'settings' => settings }, actor: { type: 'system', label: 'boot' }, source: 'service' }
35
+ payload = { data: { 'settings' => settings }, actor: { type: 'system', label: ActorCodes::BOOT }, source: 'service' }
36
36
  return PermissionForge::Audit.record(EVENT, **payload) unless configuration.audit_events == false
37
37
 
38
38
  # Auditing is off: Audit.record would store nothing, yet "was auditing