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
@@ -9,6 +9,7 @@ module PermissionForge
9
9
  class AssignmentsController < ApplicationController
10
10
  include PermissionForge::AssignmentAuthorization
11
11
  include PermissionForge::ActingAdmin
12
+ include PermissionForge::CacheConfirmation
12
13
 
13
14
  # What config.admin_assignment_authorization is asked for each member action.
14
15
  CHANGE_ACTIONS = { update: :change_expiry, destroy: :revoke, keep: :review }.freeze
@@ -30,21 +31,24 @@ module PermissionForge
30
31
  back = pf_back_or(permission_forge.person_path(@person))
31
32
  return if pf_assignment_refused?(@chosen_sets, @person.id, :grant, back)
32
33
 
33
- created = assign_chosen_sets
34
- pf_redirect back, notice: assigned_notice(created), undo: assign_undo(created, back)
34
+ created = nil
35
+ outcome = cache_outcome { created = assign_chosen_sets }
36
+ pf_redirect back, undo: assign_undo(created, back), **cache_flash_text(outcome, assigned_notice(created))
35
37
  end
36
38
 
37
39
  # Change the end date (presets) or extend by 30 days. On a locked
38
40
  # Permission Set only a change that narrows access passes
39
41
  # (EditableLock.widening_lock): for a granting set an end date set or
40
42
  # moved earlier, for a Muting Permission Set one cleared or moved later.
43
+ # Nothing chosen (the select's own first entry), or the end date the
44
+ # assignment has, changes nothing and says so (SaveNotice).
41
45
  def update
42
46
  expires_at = new_expiry(params[:expiry].to_s)
43
- return redirect_to(return_path) if expires_at == :invalid
47
+ return redirect_to(return_path, **pf_save_flash(false)) if expires_at == :invalid
44
48
  return if widening_refused?(expires_at)
45
49
 
46
- @assignment.update!(expires_at: expires_at)
47
- redirect_to return_path, notice: end_changed_notice(expires_at)
50
+ outcome = cache_outcome { pf_save_changed(@assignment, { expires_at: expires_at }, &:save!) }
51
+ redirect_to return_path, **pf_save_flash(@assignment) { cache_flash_text(outcome, end_changed_notice(expires_at)) }
48
52
  end
49
53
 
50
54
  def confirm_destroy
@@ -56,19 +60,19 @@ module PermissionForge
56
60
  # is allowed (EditableLock.widening_lock).
57
61
  def destroy
58
62
  name = person_name(@assignment.user)
59
- set_name = @assignment.permission_set.name
63
+ set_name = pf_record_name(@assignment.permission_set)
60
64
  undo = restore_undo
61
- @assignment.destroy!
62
- pf_redirect return_path, notice: t('permission_forge.flash.assignments.removed', name: name, set: set_name), undo: undo
65
+ outcome = cache_outcome { @assignment.destroy! }
66
+ pf_redirect return_path, undo: undo, **cache_flash_text(outcome, t('permission_forge.flash.assignments.removed', name: name, set: set_name))
63
67
  end
64
68
 
65
69
  # Access review "Keep": records the reviewer and the date
66
70
  # (PermissionSetUser#mark_reviewed!, one assignment.reviewed event);
67
71
  # staleness is measured from the last review.
68
72
  def keep
69
- @assignment.mark_reviewed!(reviewer: pf_acting_admin_record)
70
- redirect_to return_path, notice: t('permission_forge.flash.assignments.kept',
71
- name: person_name(@assignment.user), set: @assignment.permission_set.name)
73
+ outcome = cache_outcome { @assignment.mark_reviewed!(reviewer: pf_acting_admin_record) }
74
+ redirect_to return_path, **cache_flash_text(outcome, t('permission_forge.flash.assignments.kept',
75
+ name: person_name(@assignment.user), set: pf_record_name(@assignment.permission_set)))
72
76
  end
73
77
 
74
78
  # Undo for "Remove": recreates the assignment.
@@ -77,8 +81,9 @@ module PermissionForge
77
81
  set, user = restore_target
78
82
  return if pf_widening_refused?(pf_widening_lock(set, :grant), return_path) || pf_assignment_refused?(set, user.id, :grant, return_path)
79
83
 
80
- restore_assignment(set, user, expires_at)
81
- redirect_to return_path, notice: t('permission_forge.flash.assignments.restored', name: person_name(user), set: set.name)
84
+ outcome = cache_outcome { restore_assignment(set, user, expires_at) }
85
+ redirect_to return_path,
86
+ **cache_flash_text(outcome, t('permission_forge.flash.assignments.restored', name: person_name(user), set: pf_record_name(set)))
82
87
  end
83
88
 
84
89
  # Undo for "Assign": removes the assignments just created (narrows
@@ -88,8 +93,8 @@ module PermissionForge
88
93
  assignments = undo_assignments
89
94
  return if undo_refused?(assignments)
90
95
 
91
- PermissionForge::Audit.correlate { assignments.each(&:destroy!) }
92
- redirect_to return_path, notice: t('permission_forge.flash.assignments.undone')
96
+ outcome = cache_outcome { PermissionForge::Audit.correlate { assignments.each(&:destroy!) } }
97
+ redirect_to return_path, **cache_flash_text(outcome, t('permission_forge.flash.assignments.undone'))
93
98
  end
94
99
 
95
100
  private
@@ -104,8 +109,9 @@ module PermissionForge
104
109
  def restore_undo
105
110
  return if pf_widening_lock(@assignment.permission_set, :grant)
106
111
 
112
+ # Microseconds: end_of_day is 23:59:59.999999, and IsoTime reads up to 9 fraction digits.
107
113
  restore = { 'permission_set_id' => @assignment.permission_set_id, 'user_id' => @assignment.user_id,
108
- 'expires_at' => @assignment.expires_at&.iso8601, 'return_to' => return_path }
114
+ 'expires_at' => @assignment.expires_at&.iso8601(6), 'return_to' => return_path }
109
115
  { path: permission_forge.restore_assignments_path, method: 'post', params: restore }
110
116
  end
111
117
 
@@ -173,7 +179,9 @@ module PermissionForge
173
179
  load_sets
174
180
  @people_counts = PermissionForge::PermissionSetUser.active_assignments.group(:permission_set_id).count
175
181
  pf_load_expiry
176
- @chosen_sets = @sets.select { |set| @selected_ids.include?(set.id) && !@existing_ids.include?(set.id) }
182
+ @chosen_sets = @sets.select do |set|
183
+ @selected_ids.include?(set.id) && !@existing_ids.include?(set.id) && !@locked_ids.include?(set.id)
184
+ end
177
185
  end
178
186
 
179
187
  def load_person
@@ -192,22 +200,24 @@ module PermissionForge
192
200
  @set_query = pf_text_param(:set_q)
193
201
  @preset_set = PermissionForge::PermissionSet.find_by(id: params[:permission_set_id])
194
202
  @selected_ids = (pf_id_list(:set_ids) + [@preset_set&.id]).compact.uniq
195
- @sets = assignable_sets
203
+ @sets = listed_sets
204
+ @locked_ids = @sets.select { |set| pf_widening_lock(set, :grant) }.to_set(&:id)
196
205
  end
197
206
 
198
207
  def picker_limit
199
208
  @person ? 0 : 8
200
209
  end
201
210
 
202
- # The sets a holder can be added to: not those where that widens access
203
- # on a locked set (a locked Muting Permission Set takes holders).
204
- def assignable_sets
211
+ # Every set matching the filter. Those a holder cannot be added to (that
212
+ # widens access on a locked set; a locked Muting Permission Set takes
213
+ # holders) are listed as locked (@locked_ids) and never chosen.
214
+ def listed_sets
205
215
  scope = PermissionForge::PermissionSet.order(:is_muting, :name)
206
216
  unless @set_query.empty?
207
217
  needle = "%#{ActiveRecord::Base.sanitize_sql_like(@set_query.downcase)}%"
208
218
  scope = scope.where('LOWER(name) LIKE ?', needle).or(scope.where(id: @selected_ids))
209
219
  end
210
- scope.reject { |set| pf_widening_lock(set, :grant) }
220
+ scope.to_a
211
221
  end
212
222
 
213
223
  def validate_form
@@ -226,7 +236,7 @@ module PermissionForge
226
236
 
227
237
  def end_changed_notice(expires_at)
228
238
  t('permission_forge.flash.assignments.end_changed', name: person_name(@assignment.user),
229
- set: @assignment.permission_set.name,
239
+ set: pf_record_name(@assignment.permission_set),
230
240
  when: helpers.pf_expiry(expires_at).first)
231
241
  end
232
242
 
@@ -235,7 +245,7 @@ module PermissionForge
235
245
  def restored_expiry = pf_time_param(:expires_at)
236
246
 
237
247
  def assigned_notice(created)
238
- sets = created.map { |assignment| assignment.permission_set.name }.to_sentence
248
+ sets = helpers.pf_to_sentence(created.map { |assignment| pf_record_name(assignment.permission_set) })
239
249
  return t('permission_forge.flash.assignments.assigned', name: person_name(@person), sets: sets) if @expires_at.nil?
240
250
 
241
251
  t('permission_forge.flash.assignments.assigned_until', name: person_name(@person), sets: sets,
@@ -244,15 +254,7 @@ module PermissionForge
244
254
 
245
255
  # :invalid for a blank or unknown key (CHANGE_KEYS has no blank entry).
246
256
  def new_expiry(key)
247
- return extended_expiry if key == 'extend30'
248
- return :invalid unless PermissionForge::Admin::ExpiryPresets::CHANGE_KEYS.include?(key)
249
-
250
- PermissionForge::Admin::ExpiryPresets.expires_at_for(key)
251
- end
252
-
253
- def extended_expiry
254
- base = [@assignment.expires_at&.to_date, Time.zone.today].compact.max
255
- (base + 30).in_time_zone.end_of_day
257
+ PermissionForge::Admin::ExpiryPresets.change_to(key, from: @assignment.expires_at)
256
258
  end
257
259
 
258
260
  # What the person stops seeing when this (granting) assignment is removed.
@@ -260,7 +262,7 @@ module PermissionForge
260
262
  permission_set = @assignment.permission_set
261
263
  return [] if permission_set.is_muting
262
264
 
263
- permission_set.resources.select(&:is_active).map { |r| helpers.pf_resource_plural(r.name) }
265
+ permission_set.resources.in_effect.map { |r| helpers.pf_resource_plural(r.name) }
264
266
  end
265
267
  end
266
268
  end
@@ -8,6 +8,7 @@ module PermissionForge
8
8
  class BulkAssignmentsController < ApplicationController
9
9
  include PermissionForge::AssignmentAuthorization
10
10
  include PermissionForge::ActingAdmin
11
+ include PermissionForge::CacheConfirmation
11
12
 
12
13
  before_action :set_permission_set
13
14
  # A locked set refuses what widens access (EditableLock.widening_lock):
@@ -34,9 +35,11 @@ module PermissionForge
34
35
  return render(:new, status: :unprocessable_entity) if @expiry_error || @nobody
35
36
  return if grant_refused?
36
37
 
37
- outcome = assign(@result.to_assign)
38
+ result = assign(@result.to_assign)
39
+ # the service's one cache outcome: what the store confirmed, and its scope (CacheConfirmation)
38
40
  redirect_to assignments_path,
39
- notice: t('permission_forge.flash.bulk_assignments.assigned', count: outcome[:assigned], set: @permission_set.name)
41
+ **cache_flash(result[:cache], { notice: :assigned, alert: :assigned_cache_kept },
42
+ count: result[:assigned], set: pf_record_name(@permission_set))
40
43
  end
41
44
 
42
45
  def confirm_revoke
@@ -47,11 +50,12 @@ module PermissionForge
47
50
  ids = selected_people.map(&:id)
48
51
  return if pf_assignment_refused?(@permission_set, ids, :revoke, assignments_path)
49
52
 
50
- outcome = PermissionForge::Services::BulkPermissionSetAssignment.call(
53
+ result = PermissionForge::Services::BulkPermissionSetAssignment.call(
51
54
  permission_set: @permission_set, user_ids: ids, action: :revoke
52
55
  )
53
56
  redirect_to assignments_path,
54
- notice: t('permission_forge.flash.bulk_assignments.revoked', count: outcome[:revoked], set: @permission_set.name)
57
+ **cache_flash(result[:cache], { notice: :revoked, alert: :revoked_cache_kept },
58
+ count: result[:revoked], set: pf_record_name(@permission_set))
55
59
  end
56
60
 
57
61
  private
@@ -32,7 +32,7 @@ module PermissionForge
32
32
  end
33
33
 
34
34
  def field_label(model, name)
35
- model&.klass&.human_attribute_name(name) || name.humanize
35
+ PermissionForge::Admin::FieldLabel.for(model, name).text
36
36
  end
37
37
  end
38
38
  end
@@ -3,11 +3,15 @@ module PermissionForge
3
3
  #
4
4
  # @api private
5
5
  class MaintenanceController < ApplicationController
6
- # Removes expired assignments (the dry run is shown on Home first).
6
+ include PermissionForge::CacheConfirmation
7
+
8
+ # Removes expired assignments (the dry run is shown on Home first). The
9
+ # flash says what the cache store confirmed for the affected people, and
10
+ # when the store lives in this process only (CacheConfirmation).
7
11
  def cleanup
8
12
  result = PermissionForge::Services::ExpiredAssignmentCleaner.call
9
13
  redirect_to permission_forge.root_path,
10
- notice: t('permission_forge.flash.maintenance.cleaned', count: result[:removed])
14
+ **cache_flash(result[:cache], { notice: :cleaned, alert: :cleaned_cache_kept }, count: result[:removed])
11
15
  end
12
16
  end
13
17
  end
@@ -55,15 +55,15 @@ module PermissionForge
55
55
  return if pf_assignment_refused?(sets, @person.id, :revoke, path)
56
56
 
57
57
  count = @removable.size
58
- confirmed = cache_confirmed? { PermissionForge::Audit.correlate { @removable.each(&:destroy!) } }
58
+ outcome = cache_outcome { PermissionForge::Audit.correlate { @removable.each(&:destroy!) } }
59
59
  redirect_to permission_forge.person_path(@person),
60
- **cache_flash(confirmed, { notice: :removed_all, alert: :removed_all_cache_kept }, count: count, name: person_name)
60
+ **cache_flash(outcome, { notice: :removed_all, alert: :removed_all_cache_kept }, count: count, name: person_name)
61
61
  end
62
62
 
63
63
  def refresh_cache
64
- confirmed = cache_confirmed? { @person.invalidate_permissions if @person.respond_to?(:invalidate_permissions) }
64
+ outcome = cache_outcome { @person.invalidate_permissions if @person.respond_to?(:invalidate_permissions) }
65
65
  redirect_to permission_forge.person_path(@person, tab: pf_tab_param(TABS)),
66
- **cache_flash(confirmed, { notice: :cache_refreshed, alert: :cache_refresh_failed }, name: person_name)
66
+ **cache_flash(outcome, { notice: :cache_refreshed, alert: :cache_refresh_failed }, name: person_name)
67
67
  end
68
68
 
69
69
  # --- consent withdrawal -------------------------------------------------------
@@ -78,11 +78,11 @@ module PermissionForge
78
78
  flag = params[:flag].to_s
79
79
  return redirect_to(permission_forge.person_path(@person)) unless consent_flags.include?(flag)
80
80
 
81
- count, invalidated = consent_service(flag, dry_run: false).values_at(:revoked_sets, :invalidated)
82
- # nothing revoked means nothing to clear: that is not a failure
81
+ result = consent_service(flag, dry_run: false)
82
+ # the service's one cache outcome (nothing revoked: nothing to clear, confirmed)
83
83
  redirect_to permission_forge.person_path(@person),
84
- **cache_flash(count.zero? || invalidated, { notice: :consent_withdrawn, alert: :consent_withdrawn_cache_kept },
85
- count: count, flag: flag)
84
+ **cache_flash(result[:cache], { notice: :consent_withdrawn, alert: :consent_withdrawn_cache_kept },
85
+ count: result[:revoked_sets], flag: flag)
86
86
  end
87
87
 
88
88
  # --- erasure ----------------------------------------------------------------------
@@ -99,10 +99,9 @@ module PermissionForge
99
99
  end
100
100
 
101
101
  result = erasure_service(dry_run: false)
102
- # nil: erased inside a transaction of the host's, the cache follows at its commit
103
- confirmed = result[:cache_cleared] != false
102
+ # the service's one cache outcome (confirmed nil: erased inside a transaction of the host's, the cache follows at its commit)
104
103
  redirect_to permission_forge.people_path,
105
- **cache_flash(confirmed, { notice: :erased, alert: :erased_cache_kept }, name: person_name)
104
+ **cache_flash(result[:cache], { notice: :erased, alert: :erased_cache_kept }, name: person_name)
106
105
  end
107
106
 
108
107
  private
@@ -151,7 +150,8 @@ module PermissionForge
151
150
  def load_audit_tab
152
151
  @changes = audit.changes_by(@person.id, limit: CHANGES_SHOWN)
153
152
  @changes_capped = audit.capped?
154
- @timeline_log = PermissionForge::Admin::AuditLog.new({ views: params[:views] },
153
+ @views = pf_text_param(:views).presence # a Hash or a list is no toggle (it would reach url_for)
154
+ @timeline_log = PermissionForge::Admin::AuditLog.new({ views: @views },
155
155
  scope: PermissionForge::Admin::AuditLog.for_person(@person.id))
156
156
  @timeline = @timeline_log.page(params[:page], per_page: TIMELINE_PER_PAGE)
157
157
  @timeline_lookups = PermissionForge::Admin::AuditLookups.new(@timeline.events)
@@ -163,14 +163,14 @@ module PermissionForge
163
163
 
164
164
  def send_changes_csv
165
165
  entries = audit.changes_by(@person.id).values.flatten
166
- csv = audit.to_csv(entries, describe: helpers.method(:pf_audit_describe), person: helpers.method(:pf_audit_people))
166
+ csv = audit.to_csv(entries, describe: helpers.method(:pf_audit_csv_describe), person: helpers.method(:pf_audit_people))
167
167
  response.headers['Cache-Control'] = 'no-store' # names of people, like the log's exports
168
168
  send_data csv, filename: "permission-forge-changes-#{@person.id}.csv", type: 'text/csv'
169
169
  end
170
170
 
171
171
  def lost_resource_labels(assignments)
172
172
  assignments.map(&:permission_set).reject(&:is_muting)
173
- .flat_map { |ps| ps.resources.select(&:is_active).map { |r| helpers.pf_resource_plural(r.name) } }.uniq
173
+ .flat_map { |ps| ps.resources.in_effect.map { |r| helpers.pf_resource_plural(r.name) } }.uniq
174
174
  end
175
175
 
176
176
  # Flags gating sets this person has, plus configured flags.
@@ -20,7 +20,7 @@ module PermissionForge
20
20
  live = @permission_set.permission_set_users.active_assignments.includes(:user).order(:created_at)
21
21
  @people_count = live.count
22
22
  @impact_people = live.limit(3).map(&:user).compact
23
- @lost = @permission_set.is_muting ? [] : @permission_set.resources.select(&:is_active).map { |r| helpers.pf_resource_plural(r.name) }
23
+ @lost = @permission_set.is_muting ? [] : @permission_set.resources.in_effect.map { |r| helpers.pf_resource_plural(r.name) }
24
24
  end
25
25
 
26
26
  def load_resources_tab
@@ -31,6 +31,15 @@ module PermissionForge
31
31
  @assignments = @permission_set.permission_set_users.includes(:user).order(:created_at).to_a
32
32
  end
33
33
 
34
+ # The Conditions tab again, with the refused form's values and errors.
35
+ def render_conditions_refused
36
+ @tab = 'conditions'
37
+ @ip_text = params[:allowed_ips].to_s
38
+ load_editor
39
+ load_conditions_tab
40
+ render :show, status: :unprocessable_entity
41
+ end
42
+
34
43
  def load_conditions_tab
35
44
  @ip_text ||= helpers.pf_ip_list(@permission_set.allowed_ips).join("\n")
36
45
  configured = (PermissionForge.configuration.admin_session_flags || {}).keys.map(&:to_s)
@@ -48,22 +57,27 @@ module PermissionForge
48
57
 
49
58
  # The set's events, newest first (the log filtered by this set).
50
59
  def load_audit_timeline
51
- @timeline_log = PermissionForge::Admin::AuditLog.new({ views: params[:views] },
60
+ @timeline_log = PermissionForge::Admin::AuditLog.new({ views: @views },
52
61
  scope: PermissionForge::Admin::AuditLog.for_permission_set(@permission_set))
53
62
  @timeline = @timeline_log.page(params[:page], per_page: AUDIT_TIMELINE_PER_PAGE)
54
63
  @timeline_lookups = PermissionForge::Admin::AuditLookups.new(@timeline.events)
55
64
  end
56
65
 
66
+ # Typed like every admin parameter (ParamShapes): a Hash or a list in
67
+ # one of them is no filter, never a 500. Days are one of the offered
68
+ # counts (AuditTrail::DAY_FILTERS), else no filter.
57
69
  def load_audit_filters
58
- @who = params[:who].presence
59
- @type = params[:type].presence
60
- @days = params[:days].presence
70
+ @who = pf_text_param(:who).presence
71
+ @type = pf_text_param(:type).presence
72
+ offered = PermissionForge::Admin::AuditTrail::DAY_FILTERS
73
+ @days = pf_integer_param(:days, default: nil, range: 1..offered.max).then { |days| days if offered.include?(days) }
61
74
  @sort = params[:sort] == 'asc' ? 'asc' : 'desc'
75
+ @views = pf_text_param(:views).presence
62
76
  end
63
77
 
64
78
  def send_audit_csv
65
79
  audit = PermissionForge::Admin::AuditTrail.new
66
- csv = audit.to_csv(@entries, describe: helpers.method(:pf_audit_describe), person: helpers.method(:pf_audit_people))
80
+ csv = audit.to_csv(@entries, describe: helpers.method(:pf_audit_csv_describe), person: helpers.method(:pf_audit_people))
67
81
  response.headers['Cache-Control'] = 'no-store' # names of people, like the log's exports
68
82
  send_data csv, filename: "permission-forge-#{@permission_set.name.parameterize}-audit.csv", type: 'text/csv'
69
83
  end
@@ -1,7 +1,9 @@
1
1
  module PermissionForge
2
2
  # Group membership. Organisational only: it never gives Permission Sets.
3
3
  # Adding and removing go through the model callbacks (group_membership.*
4
- # audit events) after config.admin_assignment_authorization.
4
+ # audit events) after config.admin_assignment_authorization. No cached
5
+ # permissions change, so the toast reports no cache outcome (unlike an
6
+ # assignment: CacheConfirmation).
5
7
  #
6
8
  # @api private
7
9
  class PermissionSetGroupUsersController < ApplicationController
@@ -4,6 +4,8 @@ module PermissionForge
4
4
  #
5
5
  # @api private
6
6
  class PermissionSetGroupsController < ApplicationController
7
+ include PermissionForge::CopyRefusal
8
+
7
9
  before_action :set_group, except: %i[index create]
8
10
  before_action :reject_locked, only: %i[update destroy confirm_destroy add_set remove_set]
9
11
 
@@ -17,7 +19,7 @@ module PermissionForge
17
19
  return render_blank_name if @group.name.blank?
18
20
 
19
21
  @group.save!
20
- redirect_to permission_forge.group_path(@group), notice: t('permission_forge.flash.groups.created', name: @group.name)
22
+ redirect_to permission_forge.group_path(@group), notice: t('permission_forge.flash.groups.created', name: pf_record_name(@group))
21
23
  end
22
24
 
23
25
  def show
@@ -28,12 +30,16 @@ module PermissionForge
28
30
  @people_counts = PermissionForge::PermissionSetUser.active_assignments.group(:permission_set_id).count
29
31
  end
30
32
 
33
+ # "Group saved. No one's access changed." for a change; an unchanged
34
+ # group is not saved, and says so (SaveNotice).
31
35
  def update
32
36
  attrs = group_params
33
- return redirect_to(permission_forge.group_path(@group), alert: t('permission_forge.errors.admin.group_blank')) if blank_name?(attrs)
37
+ return redirect_to(group_page, alert: t('permission_forge.errors.admin.group_blank')) if blank_name?(attrs)
38
+
39
+ saved = pf_save_changed(@group, attrs, &:save)
40
+ return redirect_to(group_page, alert: PermissionForge::Admin::ErrorSentence.first(@group.errors)) unless saved
34
41
 
35
- @group.update!(attrs)
36
- redirect_to permission_forge.group_path(@group), notice: t('permission_forge.flash.groups.updated')
42
+ redirect_to group_page, **pf_save_flash(@group) { t('permission_forge.flash.groups.updated') }
37
43
  end
38
44
 
39
45
  def duplicate
@@ -41,13 +47,15 @@ module PermissionForge
41
47
  permission_set_group: @group, attrs: { name: t('permission_forge.permission_sets.copy_name', name: @group.name), is_editable: true },
42
48
  unlock: true
43
49
  )
44
- redirect_to permission_forge.group_path(copy), notice: t('permission_forge.flash.groups.duplicated', name: @group.name)
50
+ redirect_to permission_forge.group_path(copy), notice: t('permission_forge.flash.groups.duplicated', name: pf_record_name(@group))
51
+ rescue ActiveRecord::RecordInvalid => e
52
+ redirect_to permission_forge.group_path(@group), alert: pf_copy_refused_message(e, pf_record_name(@group))
45
53
  end
46
54
 
47
55
  def confirm_destroy; end
48
56
 
49
57
  def destroy
50
- name = @group.name
58
+ name = pf_record_name(@group)
51
59
  @group.destroy!
52
60
  redirect_to permission_forge.groups_path, notice: t('permission_forge.flash.groups.deleted', name: name)
53
61
  end
@@ -61,7 +69,7 @@ module PermissionForge
61
69
  return if pf_reject_locked!([set, leaving], permission_forge.group_path(@group))
62
70
 
63
71
  set.update!(permission_set_group: @group)
64
- redirect_to permission_forge.group_path(@group), notice: t('permission_forge.flash.groups.set_added', set: set.name)
72
+ redirect_to permission_forge.group_path(@group), notice: t('permission_forge.flash.groups.set_added', set: pf_record_name(set))
65
73
  end
66
74
 
67
75
  def remove_set
@@ -69,7 +77,7 @@ module PermissionForge
69
77
  return if pf_reject_locked!(set, permission_forge.group_path(@group))
70
78
 
71
79
  set.update!(permission_set_group: nil)
72
- redirect_to permission_forge.group_path(@group), notice: t('permission_forge.flash.groups.set_removed', set: set.name)
80
+ redirect_to permission_forge.group_path(@group), notice: t('permission_forge.flash.groups.set_removed', set: pf_record_name(set))
73
81
  end
74
82
 
75
83
  private
@@ -78,6 +86,8 @@ module PermissionForge
78
86
  @group = PermissionForge::PermissionSetGroup.find(params[:id])
79
87
  end
80
88
 
89
+ def group_page = permission_forge.group_path(@group)
90
+
81
91
  def load_index
82
92
  @groups = PermissionForge::PermissionSetGroup.includes(:permission_sets, :permission_set_group_users).order(:name)
83
93
  @ungrouped = PermissionForge::PermissionSet.where(permission_set_group_id: nil).order(:name)
@@ -91,8 +101,12 @@ module PermissionForge
91
101
  render(:index, status: :unprocessable_entity)
92
102
  end
93
103
 
104
+ # A blank name over a generated one, or a blank description over none,
105
+ # is no change (Admin::Prefill.posted).
94
106
  def group_params
95
- pf_require_hash(:group).permit(:name, :description, :is_active).tap { |attrs| strip_name!(attrs) }
107
+ attrs = pf_require_hash(:group).permit(:name, :description, :is_active).tap { |permitted| strip_name!(permitted) }
108
+ PermissionForge::Admin::Prefill.posted_attributes(@group, attrs, :name, :description)
109
+ pf_boolean_attributes(attrs, :is_active)
96
110
  end
97
111
 
98
112
  def strip_name!(attrs)
@@ -23,7 +23,7 @@ module PermissionForge
23
23
  end
24
24
 
25
25
  def update
26
- @form = PermissionForge::Admin::RuleForm.new(resource: @resource, params: rule_params)
26
+ @form = PermissionForge::Admin::RuleForm.new(resource: @resource, params: rule_params, rule: @rule)
27
27
  save_rule(:edit, 'updated')
28
28
  end
29
29
 
@@ -34,16 +34,26 @@ module PermissionForge
34
34
  render(@rule ? :edit : :new)
35
35
  end
36
36
 
37
+ # The rule being built as the save would answer it (Admin::RulePreview:
38
+ # the first error the save would show, else the sentence the Rules tab
39
+ # draws): the fragment pf_admin.js fetches as the value inputs change;
40
+ # without JS a page of its own. Changes nothing.
41
+ def preview
42
+ stored = @resource.permission_set_rules.find_by(id: params[:rule_id]) if params[:rule_id].present?
43
+ @form = PermissionForge::Admin::RuleForm.new(resource: @resource, params: rule_params, rule: stored)
44
+ @preview = PermissionForge::Admin::RulePreview.new(form: @form, resource: @resource, stored: stored)
45
+ end
46
+
37
47
  def toggle
38
48
  @rule.update!(is_active: !@rule.is_active)
39
49
  key = @rule.is_active ? 'turned_on' : 'turned_off'
40
- redirect_to rules_path, notice: t("permission_forge.flash.rules.#{key}", rule: @rule.name)
50
+ redirect_to rules_path, notice: t("permission_forge.flash.rules.#{key}", rule: pf_record_name(@rule))
41
51
  end
42
52
 
43
53
  def confirm_destroy; end
44
54
 
45
55
  def destroy
46
- name = @rule.name
56
+ name = pf_record_name(@rule)
47
57
  @rule.destroy!
48
58
  redirect_to rules_path, notice: t('permission_forge.flash.rules.deleted', rule: name)
49
59
  end
@@ -52,7 +62,8 @@ module PermissionForge
52
62
 
53
63
  # What the builder page draws: built here, read by the template.
54
64
  def rule_builder
55
- @rule_builder ||= PermissionForge::Admin::RuleBuilder.new(form: @form, resource: @resource, model_errors: @model_errors)
65
+ @rule_builder ||= PermissionForge::Admin::RuleBuilder.new(form: @form, resource: @resource, model_errors: @model_errors,
66
+ rule: @rule)
56
67
  end
57
68
 
58
69
  def set_resource
@@ -64,9 +75,9 @@ module PermissionForge
64
75
  @rule = @resource.permission_set_rules.find(params[:id])
65
76
  end
66
77
 
78
+ # The rule's chain (rule, Resource, Permission Set), or the Resource's for a new rule.
67
79
  def reject_locked
68
- locked = [@permission_set, @resource, @rule].compact.find { |r| pf_locked?(r) }
69
- pf_reject_locked!(locked, rules_path) if locked
80
+ pf_reject_locked!(@rule || @resource, rules_path)
70
81
  end
71
82
 
72
83
  def rules_path
@@ -85,11 +96,10 @@ module PermissionForge
85
96
  return render(template, status: :unprocessable_entity)
86
97
  end
87
98
 
88
- @rule.assign_attributes(@form.attributes)
89
- if @rule.save
90
- redirect_to rules_path, notice: t("permission_forge.flash.rules.#{flash_key}", rule: @rule.name)
99
+ if pf_save_changed(@rule, @form.attributes, &:save) # an unchanged rule is not saved (SaveNotice)
100
+ redirect_to rules_path, **pf_save_flash(@rule) { t("permission_forge.flash.rules.#{flash_key}", rule: pf_record_name(@rule)) }
91
101
  else
92
- @model_errors = @rule.errors.full_messages
102
+ @model_errors = @rule.errors
93
103
  render template, status: :unprocessable_entity
94
104
  end
95
105
  end