permission_forge 0.14.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (347) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +2372 -0
  3. data/README.md +76 -25
  4. data/app/controllers/concerns/permission_forge/api/crud_actions.rb +3 -2
  5. data/app/controllers/concerns/permission_forge/api/input_shapes.rb +200 -0
  6. data/app/controllers/concerns/permission_forge/api/pagination.rb +12 -5
  7. data/app/controllers/concerns/permission_forge/api/response_renderer.rb +37 -1
  8. data/app/controllers/concerns/permission_forge/api_authentication.rb +13 -2
  9. data/app/controllers/concerns/permission_forge/cache_confirmation.rb +42 -11
  10. data/app/controllers/concerns/permission_forge/change_review.rb +42 -0
  11. data/app/controllers/concerns/permission_forge/copy_refusal.rb +39 -0
  12. data/app/controllers/concerns/permission_forge/param_shapes.rb +207 -7
  13. data/app/controllers/concerns/permission_forge/save_notice.rb +69 -0
  14. data/app/controllers/permission_forge/api/audit_events_controller.rb +1 -1
  15. data/app/controllers/permission_forge/api/base_controller.rb +163 -12
  16. data/app/controllers/permission_forge/api/dashboard_controller.rb +29 -4
  17. data/app/controllers/permission_forge/api/permission_set_group_users_controller.rb +5 -2
  18. data/app/controllers/permission_forge/api/permission_set_groups_controller.rb +5 -4
  19. data/app/controllers/permission_forge/api/permission_set_rules_controller.rb +7 -5
  20. data/app/controllers/permission_forge/api/permission_set_users_controller.rb +80 -19
  21. data/app/controllers/permission_forge/api/permission_sets_controller.rb +12 -6
  22. data/app/controllers/permission_forge/api/resource_actions_controller.rb +1 -1
  23. data/app/controllers/permission_forge/api/resource_fields_controller.rb +3 -4
  24. data/app/controllers/permission_forge/api/resources_controller.rb +26 -12
  25. data/app/controllers/permission_forge/api/unmatched_routes_controller.rb +20 -0
  26. data/app/controllers/permission_forge/application_controller.rb +39 -5
  27. data/app/controllers/permission_forge/assignments_controller.rb +36 -34
  28. data/app/controllers/permission_forge/bulk_assignments_controller.rb +8 -4
  29. data/app/controllers/permission_forge/field_lookups_controller.rb +1 -1
  30. data/app/controllers/permission_forge/maintenance_controller.rb +6 -2
  31. data/app/controllers/permission_forge/people_controller.rb +14 -14
  32. data/app/controllers/permission_forge/permission_set_editor.rb +20 -6
  33. data/app/controllers/permission_forge/permission_set_group_users_controller.rb +3 -1
  34. data/app/controllers/permission_forge/permission_set_groups_controller.rb +23 -9
  35. data/app/controllers/permission_forge/permission_set_rules_controller.rb +20 -10
  36. data/app/controllers/permission_forge/permission_sets_controller.rb +23 -24
  37. data/app/controllers/permission_forge/resource_actions_controller.rb +11 -6
  38. data/app/controllers/permission_forge/resource_fields_controller.rb +37 -19
  39. data/app/controllers/permission_forge/resources_controller.rb +12 -11
  40. data/app/frontend/permission_forge/pf_admin.css +99 -17
  41. data/app/frontend/permission_forge/pf_admin.js +36 -0
  42. data/app/helpers/permission_forge/application_helper.rb +22 -72
  43. data/app/helpers/permission_forge/audit_attribution_helper.rb +77 -0
  44. data/app/helpers/permission_forge/audit_helper.rb +57 -119
  45. data/app/helpers/permission_forge/audit_records_helper.rb +103 -0
  46. data/app/helpers/permission_forge/audit_sentences_helper.rb +56 -2
  47. data/app/helpers/permission_forge/badges_helper.rb +74 -17
  48. data/app/helpers/permission_forge/changes_helper.rb +24 -6
  49. data/app/helpers/permission_forge/conditions_helper.rb +119 -0
  50. data/app/helpers/permission_forge/labels_helper.rb +134 -0
  51. data/app/helpers/permission_forge/people_helper.rb +11 -19
  52. data/app/helpers/permission_forge/rule_problems_helper.rb +73 -0
  53. data/app/helpers/permission_forge/sentences_helper.rb +119 -98
  54. data/app/helpers/permission_forge/settings_helper.rb +2 -2
  55. data/app/helpers/permission_forge/ui_helper.rb +65 -4
  56. data/app/models/permission_forge/audit_event.rb +5 -1
  57. data/app/models/permission_forge/permission_set.rb +208 -18
  58. data/app/models/permission_forge/permission_set_group.rb +10 -4
  59. data/app/models/permission_forge/permission_set_group_user.rb +3 -1
  60. data/app/models/permission_forge/permission_set_rule.rb +254 -17
  61. data/app/models/permission_forge/permission_set_user.rb +38 -3
  62. data/app/models/permission_forge/resource.rb +18 -8
  63. data/app/models/permission_forge/resource_action.rb +15 -6
  64. data/app/models/permission_forge/resource_field.rb +97 -11
  65. data/app/views/permission_forge/assignments/confirm_destroy.html.erb +4 -4
  66. data/app/views/permission_forge/assignments/new.html.erb +10 -6
  67. data/app/views/permission_forge/audit_events/index.html.erb +1 -1
  68. data/app/views/permission_forge/audit_events/show.html.erb +16 -9
  69. data/app/views/permission_forge/bulk_assignments/confirm_revoke.html.erb +3 -3
  70. data/app/views/permission_forge/bulk_assignments/new.html.erb +3 -3
  71. data/app/views/permission_forge/field_lookups/show.html.erb +2 -2
  72. data/app/views/permission_forge/home/_broken_rules_card.html.erb +35 -0
  73. data/app/views/permission_forge/home/_conditions_card.html.erb +28 -0
  74. data/app/views/permission_forge/home/_ending_card.html.erb +11 -5
  75. data/app/views/permission_forge/home/_expired_card.html.erb +1 -1
  76. data/app/views/permission_forge/home/_mask_strategies_card.html.erb +27 -0
  77. data/app/views/permission_forge/home/_privacy_card.html.erb +21 -7
  78. data/app/views/permission_forge/home/_rules_card.html.erb +1 -1
  79. data/app/views/permission_forge/home/_secret_rules_card.html.erb +33 -0
  80. data/app/views/permission_forge/home/_stale_card.html.erb +7 -5
  81. data/app/views/permission_forge/home/_tidy_card.html.erb +1 -1
  82. data/app/views/permission_forge/home/index.html.erb +5 -6
  83. data/app/views/permission_forge/people/_access_tab.html.erb +26 -19
  84. data/app/views/permission_forge/people/_audit_tab.html.erb +8 -8
  85. data/app/views/permission_forge/people/_effective_field_source.html.erb +13 -7
  86. data/app/views/permission_forge/people/_effective_tab.html.erb +16 -15
  87. data/app/views/permission_forge/people/_effective_why.html.erb +20 -16
  88. data/app/views/permission_forge/people/_why_item.html.erb +1 -1
  89. data/app/views/permission_forge/people/confirm_remove_all.html.erb +3 -3
  90. data/app/views/permission_forge/people/erasure_preview.html.erb +1 -1
  91. data/app/views/permission_forge/permission_set_groups/confirm_destroy.html.erb +3 -3
  92. data/app/views/permission_forge/permission_set_groups/index.html.erb +3 -3
  93. data/app/views/permission_forge/permission_set_groups/show.html.erb +28 -19
  94. data/app/views/permission_forge/permission_set_rules/_builder.html.erb +35 -15
  95. data/app/views/permission_forge/permission_set_rules/_preview.html.erb +17 -0
  96. data/app/views/permission_forge/permission_set_rules/confirm_destroy.html.erb +3 -3
  97. data/app/views/permission_forge/permission_set_rules/preview.html.erb +12 -0
  98. data/app/views/permission_forge/permission_sets/_assignments_tab.html.erb +37 -22
  99. data/app/views/permission_forge/permission_sets/_audit_tab.html.erb +4 -4
  100. data/app/views/permission_forge/permission_sets/_condition_problems.html.erb +15 -0
  101. data/app/views/permission_forge/permission_sets/_conditions_tab.html.erb +14 -8
  102. data/app/views/permission_forge/permission_sets/_header.html.erb +6 -5
  103. data/app/views/permission_forge/permission_sets/_records_summary.html.erb +2 -2
  104. data/app/views/permission_forge/permission_sets/_resources_tab.html.erb +4 -4
  105. data/app/views/permission_forge/permission_sets/confirm_destroy.html.erb +4 -4
  106. data/app/views/permission_forge/permission_sets/confirm_switch_mode.html.erb +3 -3
  107. data/app/views/permission_forge/permission_sets/index.html.erb +6 -6
  108. data/app/views/permission_forge/permission_sets/new.html.erb +2 -2
  109. data/app/views/permission_forge/permission_sets/review.html.erb +13 -3
  110. data/app/views/permission_forge/permission_sets/show.html.erb +7 -5
  111. data/app/views/permission_forge/resource_actions/_across_sets.html.erb +7 -10
  112. data/app/views/permission_forge/resource_actions/_effect.html.erb +5 -5
  113. data/app/views/permission_forge/resource_actions/_form.html.erb +1 -1
  114. data/app/views/permission_forge/resource_actions/_managed_note.html.erb +5 -3
  115. data/app/views/permission_forge/resource_actions/confirm_destroy.html.erb +1 -1
  116. data/app/views/permission_forge/resource_actions/new.html.erb +1 -1
  117. data/app/views/permission_forge/resource_actions/show.html.erb +2 -2
  118. data/app/views/permission_forge/resource_fields/_explain.html.erb +19 -15
  119. data/app/views/permission_forge/resource_fields/show.html.erb +21 -12
  120. data/app/views/permission_forge/resources/_actions_tab.html.erb +4 -4
  121. data/app/views/permission_forge/resources/_fields_tab.html.erb +17 -9
  122. data/app/views/permission_forge/resources/_rules_tab.html.erb +9 -6
  123. data/app/views/permission_forge/resources/_sync.html.erb +8 -5
  124. data/app/views/permission_forge/resources/confirm_destroy.html.erb +3 -3
  125. data/app/views/permission_forge/resources/new.html.erb +3 -2
  126. data/app/views/permission_forge/resources/show.html.erb +6 -6
  127. data/app/views/permission_forge/settings/_row.html.erb +2 -2
  128. data/app/views/permission_forge/settings/show.html.erb +10 -1
  129. data/app/views/permission_forge/shared/_across_sets.html.erb +8 -9
  130. data/app/views/permission_forge/shared/_audit_events.html.erb +2 -2
  131. data/app/views/permission_forge/shared/_audit_footer.html.erb +13 -2
  132. data/app/views/permission_forge/shared/_crud_cell.html.erb +6 -6
  133. data/app/views/permission_forge/shared/_mode_banners.html.erb +1 -1
  134. data/app/views/permission_forge/shared/_savebar.html.erb +4 -2
  135. data/config/locales/permission_forge.en.yml +480 -69
  136. data/config/locales/permission_forge.mk.yml +499 -79
  137. data/config/locales/permission_forge.sq.yml +509 -80
  138. data/config/routes.rb +38 -23
  139. data/lib/generators/permission_forge/install/install_generator.rb +21 -0
  140. data/lib/generators/permission_forge/install/templates/initializer.rb.tt +35 -34
  141. data/lib/generators/permission_forge/install/templates/migration.rb.tt +5 -5
  142. data/lib/permission_forge/activation_conditions.rb +200 -0
  143. data/lib/permission_forge/admin/action_label.rb +33 -0
  144. data/lib/permission_forge/admin/action_rows.rb +3 -1
  145. data/lib/permission_forge/admin/actor_label.rb +52 -0
  146. data/lib/permission_forge/admin/attention_items.rb +152 -31
  147. data/lib/permission_forge/admin/audit_log.rb +4 -7
  148. data/lib/permission_forge/admin/audit_trail.rb +33 -17
  149. data/lib/permission_forge/admin/bulk_people_matcher.rb +32 -14
  150. data/lib/permission_forge/admin/chain_entries.rb +42 -0
  151. data/lib/permission_forge/admin/change_set.rb +12 -1
  152. data/lib/permission_forge/admin/change_set_parts/base.rb +5 -4
  153. data/lib/permission_forge/admin/change_set_parts/context.rb +3 -1
  154. data/lib/permission_forge/admin/change_set_parts/field_edit.rb +4 -1
  155. data/lib/permission_forge/admin/change_set_parts/field_levels.rb +1 -1
  156. data/lib/permission_forge/admin/change_set_parts/new_actions.rb +1 -1
  157. data/lib/permission_forge/admin/change_set_parts/new_fields.rb +38 -11
  158. data/lib/permission_forge/admin/change_set_parts/resource_edit.rb +17 -1
  159. data/lib/permission_forge/admin/change_set_parts/resource_flags.rb +1 -1
  160. data/lib/permission_forge/admin/change_set_parts/set_attrs.rb +4 -0
  161. data/lib/permission_forge/admin/change_set_parts/values.rb +10 -4
  162. data/lib/permission_forge/admin/column_fields.rb +22 -21
  163. data/lib/permission_forge/admin/conditions_form.rb +47 -15
  164. data/lib/permission_forge/admin/controller_catalog.rb +42 -14
  165. data/lib/permission_forge/admin/editable_lock.rb +83 -3
  166. data/lib/permission_forge/admin/effective_access.rb +147 -48
  167. data/lib/permission_forge/admin/effective_fields.rb +75 -67
  168. data/lib/permission_forge/admin/error_sentence.rb +47 -0
  169. data/lib/permission_forge/admin/expiry_presets.rb +17 -0
  170. data/lib/permission_forge/admin/exports/holdings.rb +6 -3
  171. data/lib/permission_forge/admin/field_across_sets.rb +7 -4
  172. data/lib/permission_forge/admin/field_chain.rb +109 -0
  173. data/lib/permission_forge/admin/field_detail.rb +24 -11
  174. data/lib/permission_forge/admin/field_label.rb +109 -0
  175. data/lib/permission_forge/admin/field_level.rb +4 -68
  176. data/lib/permission_forge/admin/field_provenance.rb +132 -0
  177. data/lib/permission_forge/admin/field_rows.rb +26 -11
  178. data/lib/permission_forge/admin/last_changes.rb +14 -0
  179. data/lib/permission_forge/admin/list_sentence.rb +32 -0
  180. data/lib/permission_forge/admin/mask_strategy_choices.rb +57 -0
  181. data/lib/permission_forge/admin/model_catalog.rb +83 -39
  182. data/lib/permission_forge/admin/model_label.rb +66 -0
  183. data/lib/permission_forge/admin/people_candidates.rb +21 -9
  184. data/lib/permission_forge/admin/people_directory.rb +23 -5
  185. data/lib/permission_forge/admin/permission_set_factory.rb +6 -15
  186. data/lib/permission_forge/admin/person_label.rb +105 -16
  187. data/lib/permission_forge/admin/prefill.rb +92 -0
  188. data/lib/permission_forge/admin/record_name.rb +45 -0
  189. data/lib/permission_forge/admin/rule_builder.rb +77 -3
  190. data/lib/permission_forge/admin/rule_form.rb +44 -10
  191. data/lib/permission_forge/admin/rule_form_input.rb +27 -11
  192. data/lib/permission_forge/admin/rule_notes.rb +43 -0
  193. data/lib/permission_forge/admin/rule_preview.rb +96 -0
  194. data/lib/permission_forge/admin/sensitive_columns.rb +10 -62
  195. data/lib/permission_forge/admin/settings_catalog.rb +64 -12
  196. data/lib/permission_forge/admin/state_label.rb +97 -0
  197. data/lib/permission_forge/admin_method_override.rb +44 -0
  198. data/lib/permission_forge/api/schema.rb +322 -0
  199. data/lib/permission_forge/api/serializers/base_serializer.rb +87 -1
  200. data/lib/permission_forge/api/serializers/permission_set_group_user_serializer.rb +1 -1
  201. data/lib/permission_forge/api/serializers/permission_set_rule_serializer.rb +1 -1
  202. data/lib/permission_forge/api/serializers/permission_set_serializer.rb +5 -2
  203. data/lib/permission_forge/api/serializers/permission_set_user_serializer.rb +1 -1
  204. data/lib/permission_forge/api/serializers/resource_action_serializer.rb +1 -1
  205. data/lib/permission_forge/api/serializers/resource_field_serializer.rb +1 -1
  206. data/lib/permission_forge/api/serializers/resource_serializer.rb +1 -1
  207. data/lib/permission_forge/api_authorizations.rb +59 -0
  208. data/lib/permission_forge/api_only_admin.rb +81 -0
  209. data/lib/permission_forge/audit/actor_codes.rb +32 -0
  210. data/lib/permission_forge/audit/actor_resolver.rb +1 -2
  211. data/lib/permission_forge/audit/boot_warnings.rb +29 -9
  212. data/lib/permission_forge/audit/event_catalog.rb +16 -6
  213. data/lib/permission_forge/audit/event_query.rb +3 -3
  214. data/lib/permission_forge/audit/reason_codes.rb +25 -0
  215. data/lib/permission_forge/audit/runtime.rb +9 -2
  216. data/lib/permission_forge/audit/settings_observer.rb +2 -2
  217. data/lib/permission_forge/audit/unmasked_without_user.rb +169 -0
  218. data/lib/permission_forge/audit.rb +24 -10
  219. data/lib/permission_forge/boot_checks.rb +180 -63
  220. data/lib/permission_forge/cache/base.rb +10 -0
  221. data/lib/permission_forge/cache/null_cache.rb +5 -0
  222. data/lib/permission_forge/cache/outcome.rb +119 -0
  223. data/lib/permission_forge/cache/rails_cache.rb +6 -0
  224. data/lib/permission_forge/cache/report.rb +150 -0
  225. data/lib/permission_forge/cache/user_keys.rb +8 -4
  226. data/lib/permission_forge/cache_warnings.rb +140 -0
  227. data/lib/permission_forge/concerns/auto_permission_scoping.rb +19 -2
  228. data/lib/permission_forge/concerns/has_tenant_scoping.rb +1 -1
  229. data/lib/permission_forge/concerns/has_uuid.rb +1 -1
  230. data/lib/permission_forge/concerns/keeps_copied_values.rb +48 -0
  231. data/lib/permission_forge/concerns/masked_attributes/alias_access.rb +12 -2
  232. data/lib/permission_forge/concerns/masked_attributes/virtual_readers.rb +6 -4
  233. data/lib/permission_forge/concerns/masked_attributes.rb +150 -34
  234. data/lib/permission_forge/concerns/normalizes_label.rb +78 -0
  235. data/lib/permission_forge/concerns/permission_cache_accessor.rb +3 -3
  236. data/lib/permission_forge/concerns/permission_scoped_findable.rb +121 -29
  237. data/lib/permission_forge/concerns/permissioned_user.rb +1 -1
  238. data/lib/permission_forge/concerns/records_audit_events.rb +13 -6
  239. data/lib/permission_forge/concerns/requires_name.rb +37 -0
  240. data/lib/permission_forge/concerns/validates_column_lengths.rb +24 -0
  241. data/lib/permission_forge/concerns/validates_dependency.rb +5 -2
  242. data/lib/permission_forge/concerns/validates_required_columns.rb +74 -0
  243. data/lib/permission_forge/conditional_sets.rb +13 -6
  244. data/lib/permission_forge/configuration/admin_ui_settings.rb +18 -4
  245. data/lib/permission_forge/configuration/audit_settings.rb +2 -1
  246. data/lib/permission_forge/configuration/controller_action_settings.rb +5 -4
  247. data/lib/permission_forge/configuration/masked_attributes_settings.rb +73 -19
  248. data/lib/permission_forge/configuration/presets.rb +20 -11
  249. data/lib/permission_forge/configuration/query_scoping_settings.rb +55 -0
  250. data/lib/permission_forge/configuration/strategy_values.rb +2 -1
  251. data/lib/permission_forge/configuration.rb +41 -18
  252. data/lib/permission_forge/controller_actions/managed_index.rb +8 -7
  253. data/lib/permission_forge/database_reach.rb +93 -0
  254. data/lib/permission_forge/deprecation.rb +1 -1
  255. data/lib/permission_forge/docs/api_samples.rb +250 -0
  256. data/lib/permission_forge/docs/configuration_reference.rb +172 -0
  257. data/lib/permission_forge/encrypted_attributes.rb +150 -0
  258. data/lib/permission_forge/engine.rb +46 -14
  259. data/lib/permission_forge/execution_state/restore.rb +74 -0
  260. data/lib/permission_forge/execution_state.rb +8 -1
  261. data/lib/permission_forge/field_level.rb +123 -0
  262. data/lib/permission_forge/field_names.rb +145 -0
  263. data/lib/permission_forge/field_variants.rb +96 -0
  264. data/lib/permission_forge/generated_text.rb +95 -0
  265. data/lib/permission_forge/inspection_filters.rb +83 -0
  266. data/lib/permission_forge/managed_fields.rb +66 -0
  267. data/lib/permission_forge/masked_models.rb +25 -19
  268. data/lib/permission_forge/masking_sync.rb +100 -25
  269. data/lib/permission_forge/model_bindings.rb +30 -19
  270. data/lib/permission_forge/model_lookup.rb +42 -0
  271. data/lib/permission_forge/operators/encrypted_alias.rb +67 -0
  272. data/lib/permission_forge/operators/registry.rb +6 -0
  273. data/lib/permission_forge/operators/rule_column.rb +242 -27
  274. data/lib/permission_forge/operators/rule_path.rb +179 -0
  275. data/lib/permission_forge/operators/rule_problem.rb +309 -0
  276. data/lib/permission_forge/operators/rule_sources.rb +96 -0
  277. data/lib/permission_forge/operators/value_casters/postgresql_types.rb +237 -0
  278. data/lib/permission_forge/operators/value_casters/temporal.rb +9 -20
  279. data/lib/permission_forge/operators/value_casters.rb +61 -13
  280. data/lib/permission_forge/operators/value_typing.rb +83 -7
  281. data/lib/permission_forge/query_scoping_warnings.rb +22 -17
  282. data/lib/permission_forge/rake_audit.rb +5 -4
  283. data/lib/permission_forge/rake_dry_run.rb +32 -0
  284. data/lib/permission_forge/rake_output.rb +189 -0
  285. data/lib/permission_forge/record_read_access.rb +51 -0
  286. data/lib/permission_forge/resource_chain.rb +14 -2
  287. data/lib/permission_forge/schema_read/ordered_types.rb +72 -0
  288. data/lib/permission_forge/schema_read.rb +335 -0
  289. data/lib/permission_forge/secret_columns.rb +49 -0
  290. data/lib/permission_forge/sensitive_columns.rb +114 -0
  291. data/lib/permission_forge/services/attribute_access_guard.rb +22 -14
  292. data/lib/permission_forge/services/bulk_permission_set_assignment.rb +16 -8
  293. data/lib/permission_forge/services/concerns/clonable.rb +9 -0
  294. data/lib/permission_forge/services/concerns/settles_cache.rb +58 -0
  295. data/lib/permission_forge/services/consent_withdrawal_handler.rb +37 -19
  296. data/lib/permission_forge/services/data_subject_erasure.rb +83 -45
  297. data/lib/permission_forge/services/expired_assignment_cleaner.rb +62 -25
  298. data/lib/permission_forge/services/extra_field_defaults.rb +1 -1
  299. data/lib/permission_forge/services/field_permission_merger.rb +44 -17
  300. data/lib/permission_forge/services/filters_response_attributes.rb +20 -5
  301. data/lib/permission_forge/services/gdpr_field_auditor/text_report.rb +33 -5
  302. data/lib/permission_forge/services/gdpr_field_auditor.rb +68 -48
  303. data/lib/permission_forge/services/host_key_guard.rb +8 -5
  304. data/lib/permission_forge/services/mask_value.rb +72 -11
  305. data/lib/permission_forge/services/permission_cache_clearer.rb +36 -15
  306. data/lib/permission_forge/services/permission_hash_builder.rb +48 -19
  307. data/lib/permission_forge/services/permission_rule_applier.rb +131 -68
  308. data/lib/permission_forge/services/permission_set_cloner.rb +32 -8
  309. data/lib/permission_forge/services/permission_set_condition_evaluator.rb +38 -36
  310. data/lib/permission_forge/services/permission_set_group_cloner.rb +2 -1
  311. data/lib/permission_forge/services/permits_attributes/result.rb +31 -0
  312. data/lib/permission_forge/services/permits_attributes.rb +48 -27
  313. data/lib/permission_forge/services/resource_field_generator/row_builder.rb +96 -0
  314. data/lib/permission_forge/services/resource_field_generator.rb +71 -42
  315. data/lib/permission_forge/services/resource_with_fields_builder.rb +2 -1
  316. data/lib/permission_forge/services/rls_operator_sql.rb +83 -24
  317. data/lib/permission_forge/services/rls_policy_generator.rb +83 -39
  318. data/lib/permission_forge/services/rls_read_access_sql.rb +131 -0
  319. data/lib/permission_forge/services/security_audit/action_check.rb +49 -5
  320. data/lib/permission_forge/services/security_audit/assignment_check.rb +2 -1
  321. data/lib/permission_forge/services/security_audit/audit_trail_check.rb +1 -3
  322. data/lib/permission_forge/services/security_audit/conditions_check.rb +92 -0
  323. data/lib/permission_forge/services/security_audit/config_check.rb +41 -18
  324. data/lib/permission_forge/services/security_audit/integrity_check.rb +71 -4
  325. data/lib/permission_forge/services/security_audit/masking_without_user.rb +66 -0
  326. data/lib/permission_forge/services/security_audit/personal_data_check.rb +7 -11
  327. data/lib/permission_forge/services/security_audit/rule_check.rb +180 -50
  328. data/lib/permission_forge/services/security_audit/text_report.rb +14 -2
  329. data/lib/permission_forge/services/security_audit/unmanaged_actions.rb +145 -0
  330. data/lib/permission_forge/services/security_audit/user_model_check.rb +4 -3
  331. data/lib/permission_forge/services/security_audit.rb +16 -4
  332. data/lib/permission_forge/services/stale_assignment_auditor.rb +8 -5
  333. data/lib/permission_forge/timestamp_fields.rb +60 -0
  334. data/lib/permission_forge/unique_conflict.rb +117 -0
  335. data/lib/permission_forge/version.rb +1 -1
  336. data/lib/permission_forge/virtual_fields/discovery.rb +2 -1
  337. data/lib/permission_forge/virtual_fields.rb +15 -5
  338. data/lib/permission_forge/warnings.rb +263 -0
  339. data/lib/permission_forge/web_request.rb +248 -0
  340. data/lib/permission_forge.rb +54 -0
  341. data/lib/tasks/permission_forge.rake +26 -6
  342. data/lib/tasks/permission_forge_audit_events.rake +8 -4
  343. data/lib/tasks/permission_forge_cache.rake +12 -12
  344. data/lib/tasks/permission_forge_docs.rake +48 -0
  345. data/lib/tasks/permission_forge_expiration.rake +11 -5
  346. data/lib/tasks/permission_forge_gdpr.rake +36 -20
  347. metadata +84 -1
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Services
5
+ module SecurityAuditChecks
6
+ # Activation conditions and field rows whose read-time meaning is not
7
+ # what the row looks like: conditions the validation would refuse
8
+ # today (ActivationConditions: nobody can judge them, so a muting set
9
+ # applies to every request and a granting set never passes) and
10
+ # resources holding both a row and an explicit row for one of its
11
+ # variants (the explicit row decides since 0.15).
12
+ class ConditionsCheck < BaseCheck
13
+ LEVEL_LABELS = { 'hidden' => 'Hidden', 'masked' => 'Masked', 'read' => 'Read-only', 'edit' => 'Editable' }.freeze
14
+ # ActivationConditions#condition_problems kind => its sentence (the value, the set).
15
+ PROBLEMS = {
16
+ ip_entry: ->(value, _set) { "IP entry “#{value}” is not an IP address or CIDR range" },
17
+ ip_empty: ->(_value, _set) { 'IP list has no entry' },
18
+ flag_unusable: ->(value, _set) { "session flag #{value.inspect} holds a control character no request carries" },
19
+ hours_equal: ->(_value, set) { "start hour #{set.allowed_hours_start} equals end hour #{set.allowed_hours_end}" },
20
+ hours_incomplete: ->(_value, set) { set.allowed_hours_start.nil? ? 'start hour missing' : 'end hour missing' },
21
+ hours_range: ->(_value, _set) { 'hour outside 0–23' }
22
+ }.freeze
23
+
24
+ def call
25
+ [incomplete_conditions, variant_rows_conflict].compact
26
+ end
27
+
28
+ private
29
+
30
+ def incomplete_conditions
31
+ sets = PermissionForge::PermissionSet.with_broken_conditions # Home's card reads the same
32
+ return if sets.empty?
33
+
34
+ finding(:medium, :incomplete_conditions, 'Activation conditions nobody can judge',
35
+ 'These rows hold a condition the validation refuses today: an IP entry that is not an IP address or ' \
36
+ 'CIDR range, a list with no entry, a session flag with a control character, the same start and end ' \
37
+ 'hour, one hour of the two, or an hour outside ' \
38
+ '0–23. Such a condition cannot be judged, so a muting set applies to every request and a granting set ' \
39
+ 'never passes, with or without a request context: it grants nothing. Fix the condition on the ' \
40
+ 'set\'s Conditions tab (the admin and the API normalise the list; newline or comma lists and arrays ' \
41
+ 'are accepted).',
42
+ items: sets.map { |set| condition_item(set) })
43
+ end
44
+
45
+ def condition_item(set)
46
+ what = set.condition_problems.map { |kind, value| describe_problem(kind, value, set) }.join('; ')
47
+ effect = set.is_muting ? 'applies to every request' : 'never passes, grants nothing'
48
+ "#{set.name}#{' (muting)' if set.is_muting}: #{what} — #{effect} · #{people(set.id)}"
49
+ end
50
+
51
+ def describe_problem(kind, value, set)
52
+ PROBLEMS.fetch(kind).call(value, set)
53
+ end
54
+
55
+ # Granting resources in effect: a muting resource's rows and variants
56
+ # all mute (deny direction), so two rows there never disagree.
57
+ def variant_rows_conflict
58
+ resources = PermissionForge::Resource.in_effect.merge(PermissionForge::PermissionSet.granting)
59
+ .includes(:permission_set, :resource_fields).order(:id)
60
+ items = resources.flat_map { |resource| conflicts_of(resource) }
61
+ return if items.empty?
62
+
63
+ finding(:info, :variant_rows_conflict, 'Field rows next to an explicit row for their variant',
64
+ 'Since 0.15 an explicit row for a name decides that name: the variant derived from the other row ' \
65
+ '(owner_id → owner_uuid) no longer widens it. Check that each explicit row says what you mean.',
66
+ items: items)
67
+ end
68
+
69
+ def conflicts_of(resource)
70
+ rows = resource.resource_fields.select(&:is_active)
71
+ by_name = rows.index_by(&:name)
72
+ variants = PermissionForge::FieldVariants.new(resource.name, configuration)
73
+ rows.flat_map do |row|
74
+ variants.call(row.name).filter_map do |variant|
75
+ explicit = by_name[variant]
76
+ conflict_item(resource, row, explicit) if explicit
77
+ end
78
+ end
79
+ end
80
+
81
+ def conflict_item(resource, row, explicit)
82
+ "#{resource.permission_set.name} › #{resource.name}: #{row.name} (#{level(row)}) → " \
83
+ "#{explicit.name} (#{level(explicit)}): the #{explicit.name} row decides"
84
+ end
85
+
86
+ def level(row)
87
+ LEVEL_LABELS.fetch(PermissionForge::Admin::FieldLevel.level_of(row, muting: false))
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -6,11 +6,15 @@ module PermissionForge
6
6
  # Configuration hardening: who can reach the admin, whether the cache is
7
7
  # shared, fail-closed flags, GDPR and audit settings, and the JSON API.
8
8
  class ConfigCheck < BaseCheck
9
+ include MaskingWithoutUser
10
+
9
11
  RULES = %i[
10
12
  admin_ui_exposed process_local_cache unconfigured_fields_pass write_protection_off nested_parent_fallback
11
- nil_user_bypass masked_nil_user_bypass gdpr_defaults_off log_filtering_off audit_trail_off conditional_sets_lenient
12
- detection_raises_in_production api_without_auth api_mutations_unguarded api_sequential_ids
13
- api_rule_values_exposed tenantless_admin strict_overrides
13
+ unconfigured_actions_allowed
14
+ nil_user_bypass masked_nil_user_bypass middleware_above_web_request gdpr_defaults_off log_filtering_off audit_trail_off
15
+ conditional_sets_lenient
16
+ detection_raises_in_production api_without_auth api_mutations_unguarded api_authorization_unset
17
+ api_sequential_ids api_rule_values_exposed tenantless_admin strict_overrides
14
18
  ].freeze
15
19
 
16
20
  def call
@@ -29,12 +33,18 @@ module PermissionForge
29
33
  'a controller that authenticates and authorizes admins, or turn HTTP Basic back on.')
30
34
  end
31
35
 
36
+ # The one definition of "process-local" is the store's own
37
+ # (Cache::Base#process_local?: `cache_backend = :memory`, or
38
+ # `Rails.cache` a `:memory_store`), asked of the store the reviewed
39
+ # configuration builds.
32
40
  def process_local_cache
33
- return unless production? && configuration.cache_backend.to_s == 'memory'
41
+ return unless production? && configuration.cache_store.process_local?
34
42
 
35
43
  finding(:high, :process_local_cache, 'Permission cache is local to each process',
36
- 'cache_backend is :memory in production: a revoked permission stays cached in other Puma ' \
37
- 'workers until their TTL expires. Use :rails_cache with a shared store.')
44
+ 'The permission cache store lives in each process in production (cache_backend :memory, or ' \
45
+ 'Rails.cache a :memory_store): a revoked permission stays cached in the other Puma workers until ' \
46
+ 'cache_ttl, and an invalidation from a console, runner or rake task never reaches the servers. ' \
47
+ 'Use :rails_cache with a shared store (Redis, Memcached, Solid Cache).')
38
48
  end
39
49
 
40
50
  def unconfigured_fields_pass
@@ -64,6 +74,18 @@ module PermissionForge
64
74
  'it back to false (the default).')
65
75
  end
66
76
 
77
+ # Closed by default since 0.15: :allow is the host's explicit opt-out.
78
+ def unconfigured_actions_allowed
79
+ return unless configuration.allow_unconfigured_actions?
80
+
81
+ finding(:medium, :unconfigured_actions_allowed, 'Controller actions no Permission Set manages are allowed',
82
+ 'unconfigured_action_strategy is :allow (the opt-out from the :block default): a guarded ' \
83
+ 'controller action that maps to a Resource runs for every signed-in user until a Permission Set ' \
84
+ 'manages it, so a new action is open before anyone decides who may run it. ' \
85
+ 'unmanaged_actions_allowed lists the actions open now. Manage them, then remove the setting ' \
86
+ '(:block refuses them with 403).')
87
+ end
88
+
67
89
  def nil_user_bypass
68
90
  strategies = { nil_user_strategy: configuration.nil_user_strategy,
69
91
  response_filter_nil_user_strategy: configuration.response_filter_nil_user_strategy }
@@ -75,18 +97,6 @@ module PermissionForge
75
97
  'that touches protected models is authenticated first, or use :deny / :raise.')
76
98
  end
77
99
 
78
- # The one nil-user setting that is about the getters themselves.
79
- def masked_nil_user_bypass
80
- return if configuration.hide_masked_attributes_without_user?
81
-
82
- finding(:medium, :masked_nil_user_bypass, 'Masked attributes are readable with no current user',
83
- 'masked_attributes_nil_user_strategy is :bypass: when nobody is signed in (a job, a mailer, the ' \
84
- 'console, a resolver that returns nil) the getters, attributes and as_json of a masked model return ' \
85
- 'the stored values, whatever nil_user_strategy says. Set it to :hide (the user model stays readable, as ' \
86
- 'sign-in needs), and set the current user in ' \
87
- 'the jobs and mailers that read masked data.')
88
- end
89
-
90
100
  def gdpr_defaults_off
91
101
  return if configuration.gdpr_defaults
92
102
 
@@ -149,6 +159,19 @@ module PermissionForge
149
159
  'to refuse changes until they are set.')
150
160
  end
151
161
 
162
+ # Fails closed (each refusal is a 403 naming the option): a note, so
163
+ # that the missing lambda is found before a caller is refused.
164
+ def api_authorization_unset
165
+ lines = PermissionForge::ApiAuthorizations.lines(configuration)
166
+ return if lines.empty?
167
+
168
+ finding(:info, :api_authorization_unset, 'The JSON API refuses what no authorization lambda guards',
169
+ 'The API is on and api_auth admits callers, but an authorization lambda it needs is not set: it ' \
170
+ 'answers 403 (naming the option) to everything that lambda guards. Set each one listed, or turn ' \
171
+ 'the API off if nothing calls it.',
172
+ items: lines)
173
+ end
174
+
152
175
  # Tenancy on, and an admin base controller that sets no tenant.
153
176
  def tenantless_admin
154
177
  defaults = configuration.default_admin_controllers_under_tenancy
@@ -4,7 +4,8 @@ module PermissionForge
4
4
  module Services
5
5
  module SecurityAuditChecks
6
6
  # Rows the models' validations would refuse but the database holds
7
- # anyway (written by SQL, an older version, or another app).
7
+ # anyway (written by SQL, an older version, or another app), and field
8
+ # rows that name nothing on their model.
8
9
  class IntegrityCheck < BaseCheck
9
10
  RESOURCE_RULES = {
10
11
  'edit without read' => ->(r) { r.edit_access && !r.read_access },
@@ -18,7 +19,8 @@ module PermissionForge
18
19
  }.freeze
19
20
 
20
21
  def call
21
- [duplicate_fields, duplicate_actions, dependency_violations, unknown_models].compact
22
+ [duplicate_fields, duplicate_actions, dependency_violations, unknown_models, unknown_fields,
23
+ unknown_mask_strategies].compact
22
24
  end
23
25
 
24
26
  private
@@ -95,9 +97,74 @@ module PermissionForge
95
97
  'shares these tables it may still use them — check there before deleting.', items: names)
96
98
  end
97
99
 
100
+ # Field rows in effect whose name is nothing their model has
101
+ # (PermissionForge::FieldNames): a typo such as `emial`. Saving does
102
+ # not refuse them — a key only a serializer computes is filtered by
103
+ # a row of its name — so they are reported here. Low when a muting
104
+ # set holds one (it mutes nothing it was meant to), otherwise info (a
105
+ # granting row grants nothing on the model).
106
+ def unknown_fields
107
+ fields = unknown_field_rows
108
+ return if fields.empty?
109
+
110
+ muting = fields.any? { |field| field.resource.permission_set.is_muting }
111
+ finding(muting ? :low : :info, :unknown_fields, 'Fields naming nothing on their model',
112
+ 'No column, attribute, alias, public method, declared or discovered virtual field, or configured ' \
113
+ 'variant of the model has these names, so the rows configure nothing on it: a muting row mutes ' \
114
+ 'nothing (a typo such as emial leaves email as it was), a granting row grants nothing. Correct ' \
115
+ 'or remove the row; for a key only a serializer computes (filtered by FiltersResponseAttributes) ' \
116
+ 'declare it in the model\'s extra_fields_for_permissions (see "Extra Virtual Fields" in ' \
117
+ 'docs/field-filtering.md).',
118
+ items: fields.map { |field| unknown_field_item(field) })
119
+ end
120
+
121
+ def unknown_field_rows
122
+ fields = PermissionForge::ResourceField.in_effect.includes(resource: :permission_set).order(:resource_id, :id)
123
+ fields.group_by(&:resource).flat_map do |resource, rows|
124
+ names = PermissionForge::FieldNames.for(resource.name, siblings: rows.map(&:name), configuration: configuration)
125
+ names ? rows.reject { |field| names.include?(field.name) } : []
126
+ end
127
+ end
128
+
129
+ def unknown_field_item(field)
130
+ resource = field.resource
131
+ set = resource.permission_set
132
+ "#{set.name}#{' (muting)' if set.is_muting} › #{resource.name}.#{field.name} · #{people(set.id)}"
133
+ end
134
+
135
+ # Field rows in effect that mask with a strategy this app does not
136
+ # know (Services::MaskValue.known?): written past the model, or a
137
+ # custom strategy since removed from config.custom_mask_strategies.
138
+ # Their values are masked fully until the row or the configuration is
139
+ # fixed — medium, high when people hold the set (they read ********
140
+ # where a lighter mask was meant, and nobody is told why).
141
+ def unknown_mask_strategies
142
+ fields = unknown_strategy_rows
143
+ return if fields.empty?
144
+
145
+ held = fields.any? { |field| people_counts.fetch(field.resource.permission_set_id, 0).positive? }
146
+ finding(held ? :high : :medium, :unknown_mask_strategies, 'Fields masked with a strategy this app does not know',
147
+ 'No built-in mask strategy and no entry of config.custom_mask_strategies has these names — a row ' \
148
+ 'written past the model, or a custom strategy since removed. Such a field is masked fully ' \
149
+ '(********) until the row or the configuration is fixed: pick a strategy in the field drawer, or ' \
150
+ 'add the custom strategy back.',
151
+ items: fields.map { |field| unknown_strategy_item(field) })
152
+ end
153
+
154
+ # The reading Home's card takes too (ResourceField.with_unknown_mask_strategy).
155
+ def unknown_strategy_rows
156
+ PermissionForge::ResourceField.with_unknown_mask_strategy
157
+ end
158
+
159
+ def unknown_strategy_item(field)
160
+ resource = field.resource
161
+ "#{resource.permission_set.name} › #{resource.name}.#{field.name}: #{field.mask_strategy.inspect} · " \
162
+ "#{people(resource.permission_set_id)}"
163
+ end
164
+
165
+ # The one definition of "a model with a table" (PermissionForge::ModelLookup).
98
166
  def model?(name)
99
- klass = name.safe_constantize
100
- klass.is_a?(Class) && klass < ActiveRecord::Base
167
+ PermissionForge::ModelLookup.model?(name)
101
168
  end
102
169
  end
103
170
  end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Services
5
+ module SecurityAuditChecks
6
+ # ConfigCheck's findings about masking with nobody signed in
7
+ # (masked_attributes_nil_user_strategy) and about the request marker
8
+ # they depend on (WebRequest): included by ConfigCheck, which lists
9
+ # them in its RULES.
10
+ #
11
+ # @api private
12
+ module MaskingWithoutUser
13
+ private
14
+
15
+ # The one nil-user setting that is about the getters themselves.
16
+ # :bypass is reported with :hide_in_requests as the remedy; under
17
+ # :hide_in_requests what still reads as stored (jobs, the console) is
18
+ # an info; :hide reports nothing here (UserModelCheck notes the user
19
+ # model's exemption).
20
+ def masked_nil_user_bypass
21
+ case configuration.masked_attributes_nil_user_strategy
22
+ when :bypass then masked_nil_user_bypassed
23
+ when :hide_in_requests then masked_nil_user_outside_requests
24
+ end
25
+ end
26
+
27
+ def masked_nil_user_bypassed
28
+ finding(:medium, :masked_nil_user_bypass, 'Masked attributes are readable with no current user',
29
+ 'masked_attributes_nil_user_strategy is :bypass: when nobody is signed in the getters, attributes ' \
30
+ 'and as_json of a masked model return the stored values, whatever nil_user_strategy says — in a job, ' \
31
+ 'a mailer or the console, and also in a web request where nobody is signed in (a public page, a ' \
32
+ 'webhook, a controller that forgot authentication, a resolver that returns nil). Each such read in ' \
33
+ 'a web request is logged (once per model and process) and published as field.unmasked_without_user ' \
34
+ '(stored when audit_runtime_events includes :bypass). Set it to :hide_in_requests: hidden in web ' \
35
+ 'requests with nobody signed in, the stored values kept for jobs, rake and the console. Trade-off: ' \
36
+ 'a page that reads a masked model before anyone signs in (a tenant lookup, a public page, a mail ' \
37
+ 'sent with deliver_now) then reads hidden values: sign the request in first, or read raw columns ' \
38
+ 'there. :hide hides everywhere (set the current user in jobs and mailers). The user model stays ' \
39
+ 'readable under both, as sign-in needs.')
40
+ end
41
+
42
+ def masked_nil_user_outside_requests
43
+ finding(:info, :masked_nil_user_outside_requests, 'Masked attributes are readable with no current user outside web requests',
44
+ 'masked_attributes_nil_user_strategy is :hide_in_requests: in a web request with nobody signed in ' \
45
+ 'every guarded attribute reads as hidden, but jobs, rake tasks, the runner and the console (and ' \
46
+ 'Action Cable channel actions) read the stored values. That code runs on your servers, not on a ' \
47
+ 'visitor\'s behalf; :hide hides there too, and then needs the current user set in those jobs and mailers.')
48
+ end
49
+
50
+ # Middlewares inserted above WebRequest::Middleware read outside the
51
+ # request: :hide_in_requests does not hide there, :bypass does not
52
+ # record (WebRequest::StackCheck, also a boot warning).
53
+ def middleware_above_web_request
54
+ return if configuration.masked_attributes_nil_user_strategy == :hide
55
+
56
+ above = PermissionForge::WebRequest::StackCheck.middlewares_above
57
+ return if above.blank?
58
+
59
+ finding(:info, :middleware_above_web_request, 'Middlewares run before requests are marked',
60
+ "#{PermissionForge::WebRequest::StackCheck.message(above, 'are not hidden or recorded')} " \
61
+ 'Middlewares that read no masked model (CORS, profiling, static files) need nothing.', items: above)
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -4,7 +4,8 @@ module PermissionForge
4
4
  module Services
5
5
  module SecurityAuditChecks
6
6
  # Personal data shown unmasked, from GdprFieldAuditor. Fields an admin
7
- # marked "This is intended" in the privacy audit are left out.
7
+ # marked "This is intended" in the privacy audit are left out by the
8
+ # auditor itself (ResourceField.exposing_unacknowledged).
8
9
  class PersonalDataCheck < BaseCheck
9
10
  def call
10
11
  report = GdprFieldAuditor.call(configuration: configuration)
@@ -14,7 +15,7 @@ module PermissionForge
14
15
  private
15
16
 
16
17
  def unmasked(report)
17
- rows = report.select { |row| row[:type] == :unmasked_field && acknowledged_ids.exclude?(row[:resource_field_id]) }
18
+ rows = report.select { |row| row[:type] == :unmasked_field }
18
19
  return if rows.empty?
19
20
 
20
21
  finding(:medium, :unmasked_personal_data, 'Personal data readable unmasked',
@@ -47,7 +48,10 @@ module PermissionForge
47
48
 
48
49
  finding(:medium, :cross_set_unmasking, 'One set unmasks what another masks',
49
50
  'Masks combine with AND: anyone holding both sets sees the full value.',
50
- items: rows.map { |row| "#{row[:permission_set]} › #{row[:resource_name]}.#{row[:field_name]}" })
51
+ items: rows.map do |row|
52
+ "#{row[:permission_set]} › #{row[:resource_name]}.#{row[:field_name]} " \
53
+ "(masked in #{Array(row[:masked_in]).join(', ').presence || 'another set'})"
54
+ end)
51
55
  end
52
56
 
53
57
  def unmapped(report)
@@ -65,14 +69,6 @@ module PermissionForge
65
69
  finding(passes ? :high : :low, :unmapped_personal_data, 'Personal-data columns with no active field row', detail,
66
70
  items: rows.map { |row| "#{row[:resource]}.#{row[:column]}" })
67
71
  end
68
-
69
- def acknowledged_ids
70
- @acknowledged_ids ||= if PermissionForge::Admin::AttentionItems.acknowledgement_supported?
71
- PermissionForge::ResourceField.where.not(sensitive_acknowledged_at: nil).pluck(:id).to_set
72
- else
73
- Set.new
74
- end
75
- end
76
72
  end
77
73
  end
78
74
  end