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
@@ -150,9 +150,17 @@ module PermissionForge
150
150
  end
151
151
 
152
152
  # saved_changes minus the bookkeeping columns, JSON-ready.
153
- def audit_changes
154
- saved_changes.except(*BOOKKEEPING_COLUMNS).as_json
155
- end
153
+ def audit_changes = saved_changes.except(*BOOKKEEPING_COLUMNS).as_json
154
+
155
+ # Whether the last save wrote a change event: SaveNotice asks it.
156
+ def saved_audited_changes? = audit_changes.any?
157
+
158
+ # Whether saving now would write one (a new record always does): the
159
+ # admin saves a record only then (SaveNotice#pf_save_changed), so an
160
+ # unchanged submit runs no commit callback and invalidates nobody's
161
+ # cached permissions. Never asked by a callback: a second, unchanged
162
+ # save in a transaction must not drop the first one's invalidation.
163
+ def audited_changes_pending? = new_record? || changes_to_save.except(*BOOKKEEPING_COLUMNS).any?
156
164
 
157
165
  # The Permission Set an event about this record belongs to, if any.
158
166
  def audit_permission_set
@@ -226,10 +234,9 @@ module PermissionForge
226
234
  end
227
235
 
228
236
  def record_audit_event_for_update
229
- changes = audit_changes
230
- return if changes.empty?
237
+ return unless saved_audited_changes?
231
238
 
232
- record_audit_event(:update, audit_update_details.merge('changes' => changes))
239
+ record_audit_event(:update, audit_update_details.merge('changes' => audit_changes))
233
240
  end
234
241
 
235
242
  # Event data a model adds to its update events (the computed 'changes' wins).
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Concerns
5
+ # The one definition of "this record needs a name": a Permission Set, a
6
+ # group, a rule, a Resource, a field row and an action row. A name that
7
+ # is nil, empty or only whitespace is refused with
8
+ # `permission_forge.validations.name_blank` — in code, through the JSON
9
+ # API and in the admin alike.
10
+ #
11
+ # Checked for a new row and on a rename only: a row stored with a blank
12
+ # name before (written past the model, or before 0.15 for a rule) stays
13
+ # editable in its other attributes; the admin shows such a name as
14
+ # "Untitled …" in the page's language (Admin::RecordName).
15
+ #
16
+ # Field test round 8 found a rule saved with name "" or " " while sets
17
+ # and groups refused it: each model declared the rule itself, and the rule
18
+ # model never had it.
19
+ #
20
+ # @api private
21
+ module RequiresName
22
+ extend ActiveSupport::Concern
23
+
24
+ included do
25
+ validates :name, presence: { message: ->(*) { I18n.t('permission_forge.validations.name_blank') } },
26
+ if: :name_check_due?
27
+ end
28
+
29
+ private
30
+
31
+ # A new row, or a change to its name.
32
+ def name_check_due?
33
+ new_record? || will_save_change_to_name?
34
+ end
35
+ end
36
+ end
37
+ end
@@ -24,6 +24,7 @@ module PermissionForge
24
24
  }.freeze
25
25
 
26
26
  included do
27
+ before_validation :refuse_nul_bytes
27
28
  validate :validate_column_lengths
28
29
  end
29
30
 
@@ -38,6 +39,29 @@ module PermissionForge
38
39
 
39
40
  private
40
41
 
42
+ # A NUL byte in a string or text attribute being written: PostgreSQL
43
+ # cannot store one (its driver raises on any statement carrying it, a
44
+ # uniqueness check's included) and SQLite reads the statement cut
45
+ # short. Refused before the other validators run — they may query
46
+ # with the value — so a save is a validation error, never a raise. A
47
+ # value a copy keeps from its original (KeepsCopiedValues) is the
48
+ # original's, as stored.
49
+ def refuse_nul_bytes
50
+ names = self.class.columns.filter_map do |column|
51
+ name = column.name
52
+ name if MEASURES.key?(column.type) && will_save_change_to_attribute?(name) && nul_byte?(name)
53
+ end
54
+ return if names.empty?
55
+
56
+ names.each { |name| errors.add(name, :nul_byte, message: ->(*) { I18n.t('permission_forge.validations.nul_byte') }) }
57
+ throw :abort
58
+ end
59
+
60
+ def nul_byte?(name)
61
+ value = read_attribute(name)
62
+ value.is_a?(String) && value.include?("\0") && !(respond_to?(:copied_unchanged?, true) && copied_unchanged?(name))
63
+ end
64
+
41
65
  def validate_column_lengths
42
66
  self.class.columns.each do |column|
43
67
  name = column.name
@@ -14,7 +14,7 @@ module PermissionForge
14
14
  #
15
15
  # resource = Resource.new(edit_access: true, read_access: false)
16
16
  # resource.valid? # => false
17
- # resource.errors[:edit_access] # => ["requires read_access to be enabled"]
17
+ # resource.errors[:edit_access] # => ["requires Read to be enabled"]
18
18
  #
19
19
  # @api private
20
20
  module ValidatesDependency
@@ -44,7 +44,10 @@ module PermissionForge
44
44
  define_method(method_name) do
45
45
  return if belongs_to_muting_permission_set?
46
46
 
47
- errors.add(field, "requires #{requires} to be enabled") if send(field) && !send(requires)
47
+ return unless send(field) && !send(requires)
48
+
49
+ label = I18n.t("permission_forge.validations.attributes.#{requires}", default: requires.to_s)
50
+ errors.add(field, :requires, message: I18n.t('permission_forge.validations.requires', requires: label))
48
51
  end
49
52
  private method_name
50
53
  end
@@ -0,0 +1,74 @@
1
+ module PermissionForge
2
+ module Concerns
3
+ # Refuses nil for every column the schema declares NOT NULL as a
4
+ # validation error keyed by the attribute, so a JSON `null` or a blank
5
+ # form value (a boolean column casts "" to nil) is a 422 in the API and
6
+ # the form again in the admin, where the database raised NotNullViolation
7
+ # (a 500). Read from the schema: a NOT NULL column added later is covered
8
+ # without a change here.
9
+ #
10
+ # Left to whoever fills them: the primary key; on a new record the
11
+ # columns Rails or the gem fill when the row is created (the timestamps,
12
+ # `uuid` from HasUuid); a column that already has an error once every
13
+ # other validation ran — a presence rule's, with or without a condition
14
+ # — or whose required `belongs_to` names it as the foreign key (one
15
+ # message, not two). The check runs after every other validation of the
16
+ # model whatever the order they were declared in: an `:after` callback
17
+ # of the `validate` chain (canary `:validate_after_callback`).
18
+ #
19
+ # @api private
20
+ module ValidatesRequiredColumns
21
+ extend ActiveSupport::Concern
22
+
23
+ # Filled by the gem when a row is created (HasUuid, before_create).
24
+ FILLED_ON_CREATE = %w[uuid].freeze
25
+ # Filled by Rails when a row is created while record_timestamps is on.
26
+ TIMESTAMPS = %w[created_at created_on updated_at updated_on].freeze
27
+
28
+ included do
29
+ # After every `validate` / `validates` of the model, also those
30
+ # declared below the include or in a subclass.
31
+ set_callback :validate, :after, :validate_required_columns
32
+ end
33
+
34
+ # Whether `association` is a required belongs_to: Rails' own presence
35
+ # rule (message :required), whose condition holds whenever the
36
+ # foreign key is nil.
37
+ def self.required_association?(klass, association)
38
+ klass.validators_on(association).any? { |validator| validator.kind == :presence && validator.options[:message] == :required }
39
+ end
40
+
41
+ private
42
+
43
+ def validate_required_columns
44
+ self.class.columns.each do |column|
45
+ name = column.name
46
+ next if column.null || !has_attribute?(name) || !read_attribute(name).nil? || filled_elsewhere?(name)
47
+
48
+ errors.add(name, :blank, message: ->(*) { I18n.t('permission_forge.validations.blank') })
49
+ end
50
+ end
51
+
52
+ # Said already (an error on the attribute), or filled or checked by someone else.
53
+ def filled_elsewhere?(name)
54
+ klass = self.class
55
+ return true if errors.include?(name) || name == klass.primary_key
56
+ return true if new_record? && filled_on_create?(name)
57
+
58
+ required_foreign_key?(name)
59
+ end
60
+
61
+ def filled_on_create?(name)
62
+ FILLED_ON_CREATE.include?(name) || (self.class.record_timestamps && TIMESTAMPS.include?(name))
63
+ end
64
+
65
+ # The foreign key of a required belongs_to: its error is keyed by the association.
66
+ def required_foreign_key?(name)
67
+ klass = self.class
68
+ klass.reflect_on_all_associations(:belongs_to).any? do |reflection|
69
+ reflection.foreign_key.to_s == name && ValidatesRequiredColumns.required_association?(klass, reflection.name)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -58,7 +58,8 @@ module PermissionForge
58
58
  # - a context with an IP or session flags: those whose conditions pass
59
59
  # (Services::PermissionSetConditionEvaluator), granting and muting alike;
60
60
  # - no context at all — the resolver returned nothing (no resolver, as
61
- # shipped): every conditional set, as before;
61
+ # shipped): every conditional set, as before, but a granting set whose
62
+ # condition nobody can judge (ActivationConditions#never_grants?), which never applies;
62
63
  # - under config.conditional_sets_strict_mode, no request context — the
63
64
  # resolver returned nothing, or neither an IP nor session flags (a job,
64
65
  # the console; the time alone is not a request context): only the
@@ -104,6 +105,8 @@ module PermissionForge
104
105
  #
105
106
  # @api private
106
107
  Member = Data.define(:id, :muting, :requires_session_flag, :allowed_ips, :allowed_hours_start, :allowed_hours_end) do
108
+ include PermissionForge::ActivationConditions
109
+
107
110
  def self.of(permission_set)
108
111
  new(id: permission_set.id, muting: permission_set.is_muting == true,
109
112
  requires_session_flag: permission_set.requires_session_flag, allowed_ips: permission_set.allowed_ips,
@@ -121,13 +124,17 @@ module PermissionForge
121
124
 
122
125
  def muting? = muting
123
126
 
124
- # Whether the set counts in `context`: its conditions pass — or, in
125
- # strict mode, it is a muting set whose condition cannot be judged
126
- # there (no IP for an IP allowlist, no flags at all for a session flag).
127
+ # Whether the set counts in `context`: its conditions pass — or it is
128
+ # a muting set whose condition cannot be judged: one nobody can judge
129
+ # (ActivationConditions#conditions_broken?: an IP entry that is not an
130
+ # address, one hour of the two, the same hour twice) in every mode, or
131
+ # one the context cannot answer (no IP for an IP allowlist, no flags
132
+ # at all for a session flag) in strict mode. A lockdown that never
133
+ # locked is the defect; a granting set never passes on such a condition.
127
134
  def applies?(context, strict)
128
135
  evaluator = Services::PermissionSetConditionEvaluator
129
136
  evaluator.call(permission_set: self, context: context) ||
130
- (strict && muting && evaluator.unjudgeable?(permission_set: self, context: context))
137
+ (muting && (conditions_broken? || (strict && evaluator.unjudgeable?(permission_set: self, context: context))))
131
138
  end
132
139
  end
133
140
 
@@ -353,7 +360,7 @@ module PermissionForge
353
360
  klass = self.class
354
361
  strict = klass.strict?
355
362
  return sets.select(&:muting?) if strict && klass.no_context?(context)
356
- return sets if context.blank?
363
+ return sets.reject(&:never_grants?) if context.blank?
357
364
 
358
365
  sets.select { |member| member.applies?(context, strict) }
359
366
  end
@@ -6,6 +6,10 @@ module PermissionForge
6
6
  #
7
7
  # @api public
8
8
  module AdminUiSettings
9
+ # The default of admin_stale_assignment_days, and the threshold used
10
+ # wherever that setting is nil (the one definition of the fallback).
11
+ DEFAULT_STALE_ASSIGNMENT_DAYS = 90
12
+
9
13
  # Lambda returning the model classes offered in "Add a resource".
10
14
  # nil (default) eager-loads app/models and lists every concrete
11
15
  # ActiveRecord model with a table, excluding PermissionForge's own.
@@ -19,7 +23,9 @@ module PermissionForge
19
23
  attr_accessor :admin_session_flags
20
24
 
21
25
  # Assignments not reviewed for this many days appear under
22
- # "Stale assignments" on the admin home page. Default: 90.
26
+ # "Stale assignments" on the admin home page; also the default threshold
27
+ # of StaleAssignmentAuditor and rake permission_forge:stale_assignments. Default: 90
28
+ # (DEFAULT_STALE_ASSIGNMENT_DAYS, also used when it is set to nil).
23
29
  attr_accessor :admin_stale_assignment_days
24
30
 
25
31
  # Name of the host app shown under the logo (e.g. "Acme CRM").
@@ -53,17 +59,24 @@ module PermissionForge
53
59
  # or in middleware.
54
60
  attr_accessor :admin_tenantless_access
55
61
 
62
+ # Whether an API-only application (`config.api_only = true`) that
63
+ # mounts the engine is warned that the admin screens need the cookies,
64
+ # session and flash middlewares (default true). false for an app that
65
+ # uses only the JSON API and never opens the admin screens.
66
+ attr_accessor :api_only_admin_warning
67
+
56
68
  # Sets this group's defaults; called from Configuration#initialize.
57
69
  # @return [void]
58
70
  # @api private
59
71
  def set_admin_ui_defaults
60
72
  @admin_model_resolver = nil
61
73
  @admin_session_flags = {}
62
- @admin_stale_assignment_days = 90
74
+ @admin_stale_assignment_days = DEFAULT_STALE_ASSIGNMENT_DAYS
63
75
  @admin_app_name = nil
64
76
  @admin_back_url = nil
65
77
  @admin_docs_url = 'https://github.com/fetijashari/permission_forge/blob/main/docs/configuration.md'
66
78
  @admin_tenantless_access = :deny
79
+ @api_only_admin_warning = true
67
80
  end
68
81
 
69
82
  # @return [Boolean] whether an admin or admin API request may run with
@@ -86,7 +99,8 @@ module PermissionForge
86
99
  names.select { |_, name| name.to_s.delete_prefix('::') == 'ActionController::Base' }.keys
87
100
  end
88
101
 
89
- # The boot warning for default_admin_controllers_under_tenancy, or nil.
102
+ # The boot warning for default_admin_controllers_under_tenancy, or nil:
103
+ # the bare text (Warnings.emit puts the prefix and the severity on).
90
104
  #
91
105
  # @return [String, nil]
92
106
  def tenantless_admin_warning
@@ -99,7 +113,7 @@ module PermissionForge
99
113
  else
100
114
  'Every admin request with no tenant is refused (admin_tenantless_access :deny).'
101
115
  end
102
- '[PermissionForge] SECURITY warning: tenant_model is set but ActionController::Base, which sets no ' \
116
+ 'tenant_model is set but ActionController::Base, which sets no ' \
103
117
  "tenant, is still the base of #{names.join(' and ')}. #{consequence} Point each at a controller " \
104
118
  'that sets the current tenant (or set the tenant in middleware).'
105
119
  end
@@ -38,7 +38,8 @@ module PermissionForge
38
38
  # user, an Audit::Actor, or a Hash { type:, id:, label: }. nil (default)
39
39
  # falls back to current_user_resolver; under HTTP Basic auth the
40
40
  # supplied username is recorded with actor_type 'basic_auth'.
41
- # config.audit_actor_resolver = ->(controller) { controller.current_admin }
41
+ # # Rails 8's authentication generator, under HTTP Basic (its sign-in does not run there):
42
+ # config.audit_actor_resolver = ->(controller) { Session.find_by(id: controller.request.cookie_jar.signed[:session_id])&.user }
42
43
  attr_accessor :audit_actor_resolver
43
44
 
44
45
  # Lambda(controller) returning the request context stored with each
@@ -36,9 +36,10 @@ module PermissionForge
36
36
 
37
37
  # What the guard does with a resource or action no Permission Set manages:
38
38
  #
39
- # :allow — let it through (default — nothing is restricted until an admin
40
- # adds the action to a Permission Set)
41
- # :block — deny until a Permission Set permits it
39
+ # :block — deny until a Permission Set permits it (default since 0.15)
40
+ # :allow — let it through: nothing is restricted until an admin adds the
41
+ # action to a Permission Set (the default before 0.15; an
42
+ # explicit opt-out that fails open, reported by SecurityAudit)
42
43
  #
43
44
  # A String is taken as its Symbol; any other value raises
44
45
  # ConfigurationError (StrategyValues).
@@ -69,7 +70,7 @@ module PermissionForge
69
70
  @controller_action_discovery = false
70
71
  @controller_action_resolver = nil
71
72
  @controller_discovery_exclude = DEFAULT_CONTROLLER_DISCOVERY_EXCLUDE.dup
72
- @unconfigured_action_strategy = :allow
73
+ @unconfigured_action_strategy = :block
73
74
  @action_guard_bypass = nil
74
75
  @action_guard_nil_user_strategy = nil
75
76
  end
@@ -6,6 +6,10 @@ module PermissionForge
6
6
  #
7
7
  # @api public
8
8
  module MaskedAttributesSettings
9
+ # The masked_attributes_nil_user_strategy values that hide inside web
10
+ # requests.
11
+ HIDING_NIL_USER_STRATEGIES = %i[hide_in_requests hide].freeze
12
+
9
13
  # Value returned by MaskedAttributes when read_access is denied. Default: nil.
10
14
  attr_accessor :hidden_attribute_value
11
15
 
@@ -36,36 +40,86 @@ module PermissionForge
36
40
  attr_accessor :masked_write_protection
37
41
 
38
42
  # What the attribute guard does when there is no current user (a job, a
39
- # mailer, the console, a resolver that returns nil). Nothing else
40
- # governs masking without a user: nil_user_strategy is about rows,
41
- # response_filter_nil_user_strategy about FiltersResponseAttributes.
43
+ # mailer, the console, a public page, a resolver that returns nil).
44
+ # Nothing else governs masking without a user: nil_user_strategy is
45
+ # about rows, response_filter_nil_user_strategy about
46
+ # FiltersResponseAttributes.
47
+ #
48
+ # :bypass — every getter returns the stored value (the
49
+ # library default). Inside a web request each
50
+ # guarded attribute so returned is recorded:
51
+ # field.unmasked_without_user (runtime group
52
+ # :bypass, once per request per model and
53
+ # attribute) and one SECURITY warning per model
54
+ # and process in the log.
55
+ # :hide_in_requests — inside a web request (PermissionForge::WebRequest:
56
+ # a public page, a webhook, a controller that
57
+ # forgot authentication, the before_actions and
58
+ # middlewares that run before sign-in) every
59
+ # guarded attribute is hidden as under :hide;
60
+ # everywhere else (jobs, rake, runner, the
61
+ # console) as :bypass. What the generated
62
+ # initializer and config.preset = :strict set.
63
+ # :hide — every guarded attribute reads as
64
+ # hidden_attribute_value everywhere: getters,
65
+ # read_attribute, `[]`, attributes, as_json and
66
+ # the wrapped virtual readers and store accessors.
67
+ # Code that reads masked models with no user then
68
+ # sees hidden values: set the current user in
69
+ # those jobs and mailers.
42
70
  #
43
- # :bypass — every getter returns the stored value (default)
44
- # :hide — every guarded attribute reads as hidden_attribute_value:
45
- # getters, read_attribute, `[]`, attributes, as_json and
46
- # the wrapped virtual readers and store accessors. The
47
- # primary key and the raw paths (read_attribute_before_type_cast,
48
- # pluck) are untouched, as for a signed-in user. Code that
49
- # reads masked models with no user then sees hidden values:
50
- # set the current user in those jobs and mailers. The user
51
- # model (user_class and its subclasses) is exempt: sign-in
52
- # reads it before anyone is signed in (encrypted_password,
53
- # confirmation_token, lockable counters).
71
+ # Where it hides, the primary key and the raw paths
72
+ # (read_attribute_before_type_cast, pluck) are untouched, as for a
73
+ # signed-in user, and the user model (user_class and its subclasses) is
74
+ # exempt: sign-in reads it before anyone is signed in
75
+ # (encrypted_password, confirmation_token, lockable counters).
54
76
  attr_accessor :masked_attributes_nil_user_strategy
55
77
 
56
- # @return [Boolean] whether the strategy is :hide (whatever the model)
78
+ # @return [Boolean] whether the strategy is :hide: hidden everywhere
79
+ # (whatever the model; ask #hide_masked_attributes_without_user_for?
80
+ # for a record)
57
81
  def hide_masked_attributes_without_user?
58
- @masked_attributes_nil_user_strategy.to_s == 'hide'
82
+ @masked_attributes_nil_user_strategy == :hide
83
+ end
84
+
85
+ # @return [Boolean] whether the strategy hides guarded attributes at
86
+ # least inside web requests (:hide_in_requests or :hide)
87
+ def hide_masked_attributes_without_user_in_requests?
88
+ HIDING_NIL_USER_STRATEGIES.include?(@masked_attributes_nil_user_strategy)
59
89
  end
60
90
 
61
91
  # The one decision of whether a record of klass hides its guarded
62
- # attributes when nobody is signed in: under :hide, every model but the
63
- # user model.
92
+ # attributes when nobody is signed in: under :hide, and under
93
+ # :hide_in_requests inside a web request, every model but the user
94
+ # model.
64
95
  #
65
96
  # @param klass [Class]
66
97
  # @return [Boolean]
67
98
  def hide_masked_attributes_without_user_for?(klass)
68
- hide_masked_attributes_without_user? && !user_model_class?(klass)
99
+ masked_attributes_without_user_for(klass) == :hide
100
+ end
101
+
102
+ # What the attribute guard does with a read of klass when nobody is
103
+ # signed in, the one definition behind #hide_masked_attributes_without_user_for?:
104
+ #
105
+ # :hide — hidden (:hide; :hide_in_requests inside a web request)
106
+ # :record — returned as stored and recorded (:bypass inside a web
107
+ # request: field.unmasked_without_user), the reads
108
+ # :hide_in_requests would hide
109
+ # :bypass — returned as stored (outside a web request, and always
110
+ # for the user model)
111
+ #
112
+ # Asked on every guarded read with no user: outside a web request
113
+ # under :bypass or :hide_in_requests it reads one execution-state slot.
114
+ #
115
+ # @param klass [Class]
116
+ # @return [Symbol] :hide, :record or :bypass
117
+ def masked_attributes_without_user_for(klass)
118
+ strategy = @masked_attributes_nil_user_strategy
119
+ return :bypass unless strategy == :hide || PermissionForge::WebRequest.active?
120
+ return :bypass if user_model_class?(klass)
121
+
122
+ strategy == :bypass ? :record : :hide
69
123
  end
70
124
 
71
125
  # Sets this group's defaults; called from Configuration#initialize.
@@ -6,7 +6,8 @@ module PermissionForge
6
6
  # whether something fails open, applied as a layer of defaults under
7
7
  # the settings the host makes itself.
8
8
  #
9
- # :compat (default) — the library defaults, as every version had them
9
+ # :compat (default) — the library defaults (Configuration.new; since 0.15
10
+ # unconfigured_action_strategy is :block there too)
10
11
  # :strict — every fail-open default of the README's
11
12
  # "Production hardening checklist" closed, and the
12
13
  # settings that are already closed kept so
@@ -20,10 +21,10 @@ module PermissionForge
20
21
  # The options that need host code to close (an authenticated
21
22
  # base_controller_class, admin_assignment_authorization,
22
23
  # audit_log_authorization, a request_context_resolver, user_active_check)
23
- # are not in a preset. Nor is masked_attributes_nil_user_strategy
24
- # (OPT_IN): :hide makes jobs and mailers that read masked models with no
25
- # user see hidden values, a cost each app must choose; it stays :bypass
26
- # under every preset.
24
+ # are not in a preset. :strict sets masked_attributes_nil_user_strategy
25
+ # to :hide_in_requests (hidden inside web requests with nobody signed in,
26
+ # stored values for jobs, rake and the console); :hide, which hides in
27
+ # jobs and mailers too, counts as closed but stays each app's choice.
27
28
  #
28
29
  # @api public
29
30
  module Presets
@@ -34,36 +35,42 @@ module PermissionForge
34
35
  nil_user_strategy: :bypass,
35
36
  response_filter_nil_user_strategy: :bypass,
36
37
  auto_query_scoping: :off,
37
- unconfigured_action_strategy: :allow,
38
+ no_read_access_strategy: :rules_only,
38
39
  action_guard_nil_user_strategy: nil,
39
40
  conditional_sets_strict_mode: false,
40
41
  mask_virtual_attributes: false,
41
42
  extra_fields_default_access: :grant,
42
43
  cache_backend: :memory,
44
+ masked_attributes_nil_user_strategy: :bypass,
43
45
  # closed by default
44
46
  api_require_authorization: true,
45
47
  admin_tenantless_access: :deny,
46
48
  nested_attributes_parent_fallback: false,
47
- unconfigured_field_strategy: :block
49
+ unconfigured_field_strategy: :block,
50
+ # closed by default since 0.15 (:allow is the explicit opt-out)
51
+ unconfigured_action_strategy: :block
48
52
  }.freeze
49
53
 
50
54
  STRICT = COMPAT.merge(
51
55
  nil_user_strategy: :deny,
52
56
  response_filter_nil_user_strategy: :block,
53
57
  auto_query_scoping: :filter_only,
54
- unconfigured_action_strategy: :block,
58
+ no_read_access_strategy: :deny,
55
59
  action_guard_nil_user_strategy: :deny,
56
60
  conditional_sets_strict_mode: true,
57
61
  mask_virtual_attributes: :auto,
58
62
  extra_fields_default_access: :unconfigured,
59
- cache_backend: :rails_cache
63
+ cache_backend: :rails_cache,
64
+ masked_attributes_nil_user_strategy: :hide_in_requests
60
65
  ).freeze
61
66
 
62
67
  PRESETS = { compat: COMPAT, strict: STRICT }.freeze
63
68
 
64
69
  # Fail-open defaults of the hardening checklist that no preset closes:
65
- # an explicit choice with a documented cost.
66
- OPT_IN = %i[masked_attributes_nil_user_strategy].freeze
70
+ # an explicit choice with a documented cost. None at present
71
+ # (masked_attributes_nil_user_strategy left it in 0.15: :strict sets
72
+ # :hide_in_requests).
73
+ OPT_IN = [].freeze
67
74
  OPTIONS = COMPAT.keys.freeze
68
75
 
69
76
  # When an option counts as closed with a value other than the :strict
@@ -73,6 +80,8 @@ module PermissionForge
73
80
  # nil follows nil_user_strategy
74
81
  action_guard_nil_user_strategy: ->(_value, config) { !config.action_guard_nil_user_bypass? },
75
82
  mask_virtual_attributes: ->(value, _config) { [true, 'auto'].include?(value) },
83
+ # hidden everywhere, jobs and the console included
84
+ masked_attributes_nil_user_strategy: ->(value, _config) { value == :hide },
76
85
  # no cache at all, or a shared store the host built
77
86
  cache_backend: ->(value, _config) { value == :null || value.is_a?(ActiveSupport::Cache::Store) }
78
87
  }.freeze
@@ -18,6 +18,19 @@ module PermissionForge
18
18
  module QueryScopingSettings
19
19
  AUTO_QUERY_SCOPING_VALUES = %i[off filter_only].freeze
20
20
  UNSCOPED_QUERY_DETECTION_VALUES = %i[off log raise].freeze
21
+ # Columns a record rule never compares (PermissionForge::SecretColumns),
22
+ # by name: password digests, tokens, `encrypted_*` columns (a gem's own
23
+ # encryption, attr_encrypted), secrets, then the credentials a
24
+ # user table commonly has — passwords and their salts, API / private /
25
+ # access keys, one-time-password seeds — anchored to name segments so
26
+ # `owner_id`, `sort_key`, `secretary` or `crypto_wallet` are never caught.
27
+ # An attribute declared with Active Record's `encrypts` keeps its column
28
+ # name: it is found by its declaration, not by a pattern
29
+ # (Operators::RuleProblem :encrypted_column).
30
+ DEFAULT_RULE_SECRET_COLUMN_PATTERNS = [
31
+ /\Apassword_digest\z/, /_digest\z/, /token/, /\Aencrypted_/, /_secret/, /passw/, /\Asecret(?:_|\z)/,
32
+ /(?:\A|_)(?:api|private|secret|access)_key\z/, /(?:\A|_)crypted(?:_|\z)/, /(?:\A|_)salt\z/, /(?:\A|_)otp(?:_|\z)/
33
+ ].freeze
21
34
 
22
35
  # Level 1 — ENFORCE.
23
36
  # :off — (default) only find_by/find_by!/permission_scoped apply rules
@@ -33,6 +46,20 @@ module PermissionForge
33
46
  # Unknown values raise ConfigurationError at assignment (see below).
34
47
  attr_reader :auto_query_scoping
35
48
 
49
+ # What the record reads (find_by / find_by! / permission_scoped, and the
50
+ # default scope under auto_query_scoping) do for a signed-in user whose
51
+ # entry for the model's Resource chain does not grant read_access (no
52
+ # entry at all included):
53
+ # :rules_only — (default) apply the rules of the user's entries, if
54
+ # any: no rules, every row (can_read? is the check)
55
+ # :deny — no rows: permission_scoped and the default scope are
56
+ # empty, find_by / find_by! raise NotAuthorizedError for
57
+ # an existing record (nil / RecordNotFound for a missing
58
+ # one). The strict preset's value.
59
+ # With nobody signed in nil_user_strategy decides. One definition:
60
+ # PermissionForge::RecordReadAccess.
61
+ attr_accessor :no_read_access_strategy
62
+
36
63
  # Level 2 — DETECT.
37
64
  # :off — (default) no detection
38
65
  # :log — Rails.logger.warn for each unscoped query on a protected table
@@ -59,6 +86,25 @@ module PermissionForge
59
86
  # requires namespaced custom settings).
60
87
  attr_accessor :rls_session_variable
61
88
 
89
+ # The column names a record rule may not compare (PermissionForge::
90
+ # SecretColumns, matched against the column name in lower case): a new
91
+ # rule on one, or a change of a rule's column or path to one, is
92
+ # refused, and the admin's rule builder does not offer them. Rules
93
+ # stored before keep evaluating; the audit and Home list them. `[]`
94
+ # opts out.
95
+ attr_reader :rule_secret_column_patterns
96
+
97
+ # @param patterns [Array<Regexp>]
98
+ # @raise [ConfigurationError] for anything that is not a list of regular expressions
99
+ def rule_secret_column_patterns=(patterns)
100
+ unless patterns.is_a?(Array) && patterns.all?(Regexp)
101
+ raise PermissionForge::ConfigurationError,
102
+ "Invalid rule_secret_column_patterns: #{patterns.inspect}. Give a list of regular expressions ([] opts out)."
103
+ end
104
+
105
+ @rule_secret_column_patterns = patterns
106
+ end
107
+
62
108
  def auto_query_scoping=(value)
63
109
  @auto_query_scoping = normalize_scoping_value(
64
110
  value, AUTO_QUERY_SCOPING_VALUES, 'auto_query_scoping'
@@ -75,6 +121,13 @@ module PermissionForge
75
121
  @auto_query_scoping == :filter_only
76
122
  end
77
123
 
124
+ # @return [Boolean] whether the record reads of a signed-in user need
125
+ # read_access on the model's Resource chain. Only the exact
126
+ # :rules_only value applies the rules alone.
127
+ def deny_record_reads_without_read_access?
128
+ no_read_access_strategy.to_s != 'rules_only'
129
+ end
130
+
78
131
  def unscoped_query_detection_active?
79
132
  return false if @unscoped_query_detection == :off
80
133
  return false unless defined?(Rails) && Rails.respond_to?(:env)
@@ -86,10 +139,12 @@ module PermissionForge
86
139
 
87
140
  def set_query_scoping_defaults
88
141
  @auto_query_scoping = :off
142
+ @no_read_access_strategy = :rules_only
89
143
  @unscoped_query_detection = :off
90
144
  @unscoped_query_detection_environments = %w[development test]
91
145
  @database_row_level_security = false
92
146
  @rls_session_variable = 'permission_forge.user_id'
147
+ @rule_secret_column_patterns = DEFAULT_RULE_SECRET_COLUMN_PATTERNS.dup
93
148
  end
94
149
 
95
150
  # Fail closed on typos: an unknown value must never silently enable a