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
@@ -26,6 +26,22 @@ module PermissionForge
26
26
  next unless ended
27
27
 
28
28
  t('permission_forge.audit_log.sentence_variants.assignment_revoked_expired', **parts, date: pf_date(ended))
29
+ },
30
+ # A clear the cache store did not confirm is not told as cleared.
31
+ 'cache.cleared' => lambda { |event, parts|
32
+ next unless pf_audit_cache_unconfirmed?(event)
33
+
34
+ t('permission_forge.audit_log.sentence_variants.cache_cleared_unconfirmed', **parts)
35
+ },
36
+ 'permissions.invalidated' => lambda { |event, parts|
37
+ next unless pf_audit_cache_unconfirmed?(event)
38
+
39
+ t('permission_forge.audit_log.sentence_variants.permissions_invalidated_unconfirmed', **parts)
40
+ },
41
+ 'cache.settled' => lambda { |event, parts|
42
+ next unless pf_audit_cache_unconfirmed?(event)
43
+
44
+ t('permission_forge.audit_log.sentence_variants.cache_settled_unconfirmed', **parts)
29
45
  }
30
46
  }.freeze
31
47
 
@@ -33,13 +49,51 @@ module PermissionForge
33
49
  # (permission_forge.audit_log.sentences.<event>): a new event needs one
34
50
  # key in each locale, and an event without one (a host's own) still reads
35
51
  # as "<actor>: <event name>". Interpolations: actor, record, set, person,
36
- # group, event, model (a runtime event's model).
52
+ # group, event, model (a runtime event's model). An event whose cache
53
+ # work reached a per-process store (its Cache::Outcome:
54
+ # data['process_local']) ends with the one cache-scope suffix, whatever
55
+ # the event: which processes kept their entries depends on where it ran
56
+ # (data['in_request']).
37
57
  def pf_audit_sentence(event, lookups)
38
58
  parts = pf_audit_sentence_parts(event, lookups)
39
59
  variant = AUDIT_SENTENCE_VARIANTS[event.event]
40
60
  variant &&= instance_exec(event, parts, &variant)
41
- return variant if variant
61
+ sentence = variant || pf_audit_plain_sentence(event, parts)
62
+ suffix = pf_audit_cache_scope_suffix(event)
63
+ suffix ? "#{sentence} #{suffix}" : sentence
64
+ end
65
+
66
+ # Whether the event's cache work reached a per-process in-memory store
67
+ # (data['process_local'], Cache::Base#process_local?).
68
+ def pf_audit_process_local?(event)
69
+ event.data.is_a?(Hash) && event.data['process_local'] == true
70
+ end
71
+
72
+ # Whether the store said no to the event's cache work: its Cache::Outcome
73
+ # (`cache_confirmed`), or a permissions.invalidated event's `confirmed`.
74
+ def pf_audit_cache_unconfirmed?(event)
75
+ data = event.data
76
+ data.is_a?(Hash) && (data['cache_confirmed'] == false || data['confirmed'] == false)
77
+ end
78
+
79
+ # The suffix for a per-process store, from where the work ran
80
+ # (Cache::Outcome#kept_by) and what the store said together: cleared in
81
+ # a server process (the other web workers and the job processes kept
82
+ # theirs), cleared in the process that ran it (every other process kept
83
+ # theirs — also what an event recorded without `in_request` reads as:
84
+ # it never names the web processes), or not confirmed (nothing is said
85
+ # to be cleared anywhere).
86
+ def pf_audit_cache_scope_suffix(event)
87
+ return unless pf_audit_process_local?(event)
88
+
89
+ scope = if pf_audit_cache_unconfirmed?(event) then 'unconfirmed'
90
+ elsif event.data['in_request'] == true then 'server_process'
91
+ else 'own_process'
92
+ end
93
+ t("permission_forge.audit_log.cache_scope.#{scope}")
94
+ end
42
95
 
96
+ def pf_audit_plain_sentence(event, parts)
43
97
  fallback = t("#{AUDIT_SENTENCES}.fallback", **parts)
44
98
  sentence = t("#{AUDIT_SENTENCES}.#{event.event}", **parts, default: fallback)
45
99
  sentence.is_a?(String) ? sentence : fallback
@@ -4,8 +4,13 @@ module PermissionForge
4
4
  #
5
5
  # @api private
6
6
  module BadgesHelper
7
- LEVEL_GLYPHS = { 'hidden' => '∅', 'masked' => '••', 'read' => '◉', 'edit' => '✎', 'leave' => '–',
8
- 'mask' => '••', 'hide' => '⊘', 'muted' => '⊘', 'none' => '–' }.freeze
7
+ # The level-badge colour of a state that is not a field level
8
+ # (Admin::StateLabel kinds :effective and :action), by its kind.
9
+ LEVEL_BADGE_CSS = {
10
+ effective: { 'hidden_by_muting' => 'muted' },
11
+ action: { 'permitted' => 'edit', 'not_permitted' => 'none', 'blocked' => 'muted', 'unchanged' => 'leave',
12
+ 'none' => 'none' }
13
+ }.freeze
9
14
 
10
15
  MASK_SAMPLES = [
11
16
  [/email/, 'james.walker@acme.com'],
@@ -35,8 +40,19 @@ module PermissionForge
35
40
  content_tag(:span, content, class: classes, title: opts[:title])
36
41
  end
37
42
 
38
- def pf_lock_badge(title: pf_ui('locked.title'))
39
- pf_badge([pf_lock_icon, pf_ui('badges.locked')], title: title)
43
+ # The Locked badge. With a record: only when it is locked (pf_locked?,
44
+ # the chain), so a row's badge and its disabled controls agree.
45
+ def pf_lock_badge(record = nil, title: pf_ui('locked.title'))
46
+ return if !record.nil? && !pf_locked?(record)
47
+
48
+ pf_badge([pf_lock_icon, pf_state(:lock, :locked).word], title: title)
49
+ end
50
+
51
+ # Whether a record is locked: itself, its Resource, its Permission Set or
52
+ # (a membership) its group flagged is_editable: false — Admin::EditableLock,
53
+ # the one definition the controllers refuse by.
54
+ def pf_locked?(record)
55
+ PermissionForge::Admin::EditableLock.locked?(record)
40
56
  end
41
57
 
42
58
  def pf_off_badge
@@ -61,14 +77,10 @@ module PermissionForge
61
77
  title: pf_condition_sentence(permission_set))
62
78
  badges << pf_fail_open_badge if pf_fail_open?
63
79
  end
64
- badges << pf_lock_badge if pf_locked_record?(permission_set)
80
+ badges << pf_lock_badge(permission_set)
65
81
  safe_join(badges, ' ')
66
82
  end
67
83
 
68
- def pf_locked_record?(record)
69
- record.respond_to?(:is_editable) && record.is_editable == false
70
- end
71
-
72
84
  # Permission Set chip. state: :normal / :inactive / :expired (auto from set).
73
85
  def pf_set_chip(permission_set, large: false, href: :auto, expired: false)
74
86
  state = pf_set_chip_state(permission_set, expired)
@@ -87,23 +99,68 @@ module PermissionForge
87
99
 
88
100
  def pf_set_chip_body(permission_set, state)
89
101
  suffix_key = CHIP_SUFFIX_KEYS[state]
90
- safe_join([content_tag(:span, "#{CHIP_PREFIXES[state]}#{permission_set.name}"),
102
+ safe_join([content_tag(:span, "#{CHIP_PREFIXES[state]}#{pf_record_name(permission_set)}"),
91
103
  (content_tag(:span, pf_ui(suffix_key), class: 'pf-chip__suffix') if suffix_key)].compact, ' ')
92
104
  end
93
105
 
94
- # Effective/configured field level badge ("•• Masked · j***@acme.com").
95
- def pf_level_badge(level, strategy: nil, sample_for: nil, label: nil)
96
- level = level.to_s
97
- text = label || pf_level_label(level)
98
- if %w[masked mask].include?(level) && strategy.present?
106
+ # A field's level for one person (an EffectiveAccess::FieldOutcome), the
107
+ # same in their Effective access row, its Why panel and the field
108
+ # drawer's "Explain for": hidden by a Muting Permission Set reads
109
+ # "⊘ Hidden by muting", never the neutral "∅ Hidden" of a field no set
110
+ # grants. `sample: true` adds a masked value's preview (the Why panel).
111
+ def pf_effective_field_badge(field, sample: false)
112
+ return pf_level_badge('hidden_by_muting', kind: :effective) if field.hidden_by_muting?
113
+
114
+ masked = field.level == 'masked'
115
+ pf_level_badge(field.level, strategy: (field.mask_strategy if masked), sample_for: (field.name if masked && sample))
116
+ end
117
+
118
+ # [glyph, sentence] for the entry along a single-table-inheritance chain
119
+ # that decides a person's field (EffectiveAccess::FieldOutcome#decided_by,
120
+ # #unreadable, #reason), or nil when the resource's own entry decides and
121
+ # grants Read: the Why panel and the field drawer's "Explain for" both
122
+ # say it, and the Effective access row says it when it is the reason
123
+ # (FieldOutcome#reason :unreadable or :inherited, B#39).
124
+ def pf_field_chain_reason(field)
125
+ base = field.decided_by
126
+ if field.unreadable
127
+ return ['∅', t('permission_forge.people.effective_why.field_own_unreadable')] unless base
128
+
129
+ return ['∅', t('permission_forge.people.effective_why.field_base_unreadable', resource: pf_resource_label(base))]
130
+ end
131
+ return unless base
132
+ return ['↑', t('permission_forge.people.effective_why.field_hidden_on', resource: pf_resource_label(base))] if field.reason == :inherited
133
+
134
+ ['↑', t('permission_forge.people.effective_why.field_decided_by', resource: pf_resource_label(base))]
135
+ end
136
+
137
+ # A level as a badge ("•• Masked (Email) · j***@acme.com"): a field level
138
+ # by default, or a state of another kind (Admin::StateLabel) drawn the
139
+ # same way — an effective outcome, an action state.
140
+ def pf_level_badge(level, strategy: nil, sample_for: nil, kind: :field_level)
141
+ label = PermissionForge::Admin::StateLabel.for(kind, level)
142
+ text = label.word
143
+ if %w[masked mask].include?(label.state) && strategy.present?
99
144
  text = "#{text} (#{pf_mask_strategy_label(strategy)})"
100
145
  text = "#{text} · #{pf_mask_preview(sample_for, strategy)}" if sample_for
101
146
  end
102
- content_tag(:span, "#{LEVEL_GLYPHS.fetch(level, '–')} #{text}", class: "pf-level-badge pf-level-badge--#{level}")
147
+ css = LEVEL_BADGE_CSS.fetch(kind.to_sym, {}).fetch(label.state, label.state)
148
+ content_tag(:span, "#{label.glyph} #{text}", class: "pf-level-badge pf-level-badge--#{css}")
103
149
  end
104
150
 
151
+ # The word of a field level (Admin::StateLabel, kind :field_level).
105
152
  def pf_level_label(level)
106
- pf_ui("levels.#{level}")
153
+ pf_state(:field_level, level).word
154
+ end
155
+
156
+ # The caption of a Fields tab filter: a level's word, Sensitive, or "All".
157
+ def pf_field_filter_label(filter)
158
+ case filter.to_s
159
+ when 'all' then t('permission_forge.resources.fields_tab.filters.all')
160
+ when 'sensitive' then pf_ui('badges.sensitive')
161
+ when 'unconfigured' then pf_state(:field_level, :none).word
162
+ else pf_level_label(filter)
163
+ end
107
164
  end
108
165
 
109
166
  def pf_mask_strategy_label(strategy)
@@ -42,18 +42,27 @@ module PermissionForge
42
42
  "#{text} #{pf_action_managed_note(resource, action.name)}"
43
43
  end
44
44
 
45
- # The first row for an action makes it managed for everyone; later rows
46
- # join the sets that already manage it.
45
+ # The first row for an action makes it managed for everyone (under :allow
46
+ # that restricts it; under :block, the default, it opens it to the people
47
+ # permitted); later rows join the sets that already manage it.
47
48
  def pf_action_managed_note(resource, key)
48
49
  return pf_ui('changes.action_managed_later') unless resource.in_effect?
49
50
 
50
51
  across = pf_action_across(resource.name)
51
- return pf_ui('changes.action_managed_first') unless across.managed?(key)
52
+ return pf_action_managed_first unless across.managed?(key)
52
53
 
53
54
  sets = across.permitting_sets(key).map(&:name)
54
55
  sets.any? ? pf_ui('changes.action_managed_already', sets: sets.join(', ')) : pf_ui('changes.action_managed_nobody')
55
56
  end
56
57
 
58
+ def pf_action_managed_first
59
+ if PermissionForge.configuration.allow_unconfigured_actions?
60
+ pf_ui('changes.action_managed_first_allowed')
61
+ else
62
+ pf_ui('changes.action_managed_first_blocked')
63
+ end
64
+ end
65
+
57
66
  # One lookup per resource name per page.
58
67
  def pf_action_across(resource_name)
59
68
  (@pf_action_across ||= {})[resource_name] ||= PermissionForge::Admin::ActionAcrossSets.new(resource_name)
@@ -64,7 +73,7 @@ module PermissionForge
64
73
  when 'is_active' then pf_ui(change.to ? 'changes.set_on' : 'changes.set_off')
65
74
  when 'permission_set_group_id'
66
75
  group = change.to && PermissionForge::PermissionSetGroup.find_by(id: change.to)
67
- group ? pf_ui('changes.set_group', group: group.name) : pf_ui('changes.set_no_group')
76
+ group ? pf_ui('changes.set_group', group: pf_record_name(group)) : pf_ui('changes.set_no_group')
68
77
  else pf_ui('changes.set_text', attribute: pf_ui("changes.attributes.#{change.attribute}"))
69
78
  end
70
79
  end
@@ -76,6 +85,13 @@ module PermissionForge
76
85
  change.cascaded ? "#{text} #{pf_ui('changes.cascaded')}" : text
77
86
  end
78
87
 
88
+ # A flag the request turned on that stays off (ChangeSet#refused):
89
+ # "Customer: Edit not turned on, because Read is off".
90
+ def pf_refusal_description(refusal)
91
+ pf_ui("changes.not_applied_#{refusal.needs}", resource: pf_resource_label(refusal.resource),
92
+ flag: pf_flag_name(refusal.attribute))
93
+ end
94
+
79
95
  def pf_flag_name(flag)
80
96
  pf_ui("flags_short.#{flag}")
81
97
  end
@@ -99,15 +115,17 @@ module PermissionForge
99
115
  %w[masked mask].include?(level) && strategy ? "#{text} (#{pf_mask_strategy_label(strategy)})" : text
100
116
  end
101
117
 
118
+ # A field row named as everywhere else (Admin::FieldLabel: a label the
119
+ # gem generated is no label).
102
120
  def pf_change_field_name(field)
103
- field.label.presence || field.name
121
+ pf_field_label_text(field.resource&.name, field.name, row: field)
104
122
  end
105
123
 
106
124
  def pf_field_attr_change(change)
107
125
  field = change.record
108
126
  if change.attribute == 'is_active'
109
127
  return pf_ui(change.to ? 'changes.field_on' : 'changes.field_off', resource: pf_resource_label(change.resource),
110
- field: field.label.presence || field.name)
128
+ field: pf_change_field_name(field))
111
129
  end
112
130
 
113
131
  pf_ui('changes.field_text', resource: pf_resource_label(change.resource), field: field.name,
@@ -0,0 +1,119 @@
1
+ module PermissionForge
2
+ # Plain-language sentences for a Permission Set's activation conditions,
3
+ # read through ActivationConditions — the definition that judges them
4
+ # (round 7, E#32 / B#48a): the summary ("Only works from … between …"),
5
+ # its form on the set's own Conditions tab (`here:`), the stored IP
6
+ # entries marked as host data (`markup:`), the problems the validations
7
+ # refuse (#pf_condition_problem) and what such a condition does
8
+ # (#pf_condition_effect, shared with Home's card).
9
+ #
10
+ # @api private
11
+ module ConditionsHelper
12
+ # `here`: the sentence is drawn on the set's own Conditions tab, so a
13
+ # broken condition is not sent "to the Conditions tab" (E#32).
14
+ # `markup`: the stored IP entries in <code> (host data, marked); plain
15
+ # text for an attribute (a title).
16
+ def pf_condition_parts(permission_set, here: false, markup: false)
17
+ [pf_condition_ips(permission_set, here: here, markup: markup), pf_condition_hours(permission_set, here: here),
18
+ pf_condition_flag(permission_set)].compact
19
+ end
20
+
21
+ # The IP entries as the engine reads them (ActivationConditions); a list
22
+ # it cannot judge (an entry that is no address, no entry at all) says so
23
+ # (B#48a), never reads as a list of addresses.
24
+ def pf_condition_ips(permission_set, here: false, markup: false)
25
+ conditions = PermissionForge::ActivationConditions.of(permission_set)
26
+ return if conditions.allowed_ip_list.empty? && !conditions.ip_list_broken?
27
+
28
+ key = if conditions.ip_list_broken? then here ? 'conditions.ips_broken_here' : 'conditions.ips_broken'
29
+ else 'conditions.from_ips'
30
+ end
31
+ listed = pf_condition_ip_list(conditions, permission_set, markup)
32
+ markup ? pf_t_markup(ips: listed) { |held| pf_ui(key, **held) } : pf_ui(key, ips: listed)
33
+ end
34
+
35
+ # The entries as one list ("10.0.0.0/8 or office-network"), each in
36
+ # <code> with `markup`; without it (an attribute, which cannot mark host
37
+ # data) the stored text as written; a list of separators alone as written.
38
+ def pf_condition_ip_list(conditions, permission_set, markup)
39
+ return permission_set.allowed_ips.to_s.strip unless markup
40
+
41
+ ips = conditions.allowed_ip_list.presence || [permission_set.allowed_ips.to_s.strip]
42
+ pf_condition_code_list(ips.map { |ip| content_tag(:code, ip, class: 'pf-mono-key') })
43
+ end
44
+
45
+ # "a, b or c" from markup, with the page's connectors.
46
+ def pf_condition_code_list(codes)
47
+ return codes.first if codes.one?
48
+
49
+ words = PermissionForge::Admin::ListSentence.connectors[:words_connector]
50
+ safe_join([safe_join(codes[0..-2], words), codes.last], pf_ui('conditions.or'))
51
+ end
52
+
53
+ # The hour window as the engine reads it: none, a window, an overnight
54
+ # one, or a broken one (one hour of the two, the same hour twice, an
55
+ # hour outside 0–23) that nobody can judge — said so, never shown as an
56
+ # overnight window.
57
+ def pf_condition_hours(permission_set, here: false)
58
+ window = PermissionForge::ActivationConditions.of(permission_set).hours_window
59
+ return unless window.set?
60
+
61
+ hours = { from: pf_hour_or_blank(window.from), to: pf_hour_or_blank(window.to) }
62
+ return pf_ui(here ? 'conditions.hours_broken_here' : 'conditions.hours_broken', **hours) if window.broken?
63
+
64
+ pf_ui("conditions.#{window.wraps? ? 'hours_overnight' : 'hours'}", **hours)
65
+ end
66
+
67
+ def pf_hour_or_blank(hour)
68
+ hour.nil? ? pf_ui('conditions.no_hour') : pf_hour(hour)
69
+ end
70
+
71
+ # One sentence per ActivationConditions#condition_problems entry: what
72
+ # cannot be judged, the stored entry as written (in <code>).
73
+ def pf_condition_problem(kind, value)
74
+ case kind
75
+ when :ip_entry then pf_ui('conditions.problems.ip_entry_html', entry: content_tag(:code, value, class: 'pf-mono-key'))
76
+ when :flag_unusable then pf_ui('conditions.problems.flag_unusable_html', flag: content_tag(:code, value.inspect, class: 'pf-mono-key'))
77
+ else pf_ui("conditions.problems.#{kind}")
78
+ end
79
+ end
80
+
81
+ # What a condition nobody can judge does to the set (ActivationConditions,
82
+ # ConditionalSets::Member): a granting set never passes, a muting set
83
+ # applies to every request — the words Home's card uses.
84
+ def pf_condition_effect(permission_set)
85
+ pf_ui("conditions.effect.#{permission_set.is_muting ? 'muting' : 'granting'}")
86
+ end
87
+
88
+ def pf_condition_flag(permission_set)
89
+ flag = permission_set.requires_session_flag
90
+ pf_flag_phrase(flag) if flag.present?
91
+ end
92
+
93
+ # The set's conditions as one sentence; `markup` draws the stored IP
94
+ # entries in <code> (a page's text), without it plain text (a title).
95
+ def pf_condition_sentence(permission_set, here: false, markup: false)
96
+ parts = pf_condition_parts(permission_set, here: here, markup: markup)
97
+ return pf_ui('conditions.always') if parts.empty?
98
+ return pf_ui('conditions.only_works', parts: parts.join(', ')) unless markup
99
+
100
+ pf_t_markup(parts: safe_join(parts, ', ')) { |held| pf_ui('conditions.only_works', **held) }
101
+ end
102
+
103
+ # The entries of an IP list as written (ActivationConditions.entries).
104
+ def pf_ip_list(value)
105
+ PermissionForge::ActivationConditions.entries(value)
106
+ end
107
+
108
+ def pf_flag_label(flag)
109
+ configured = PermissionForge.configuration.admin_session_flags || {}
110
+ configured[flag.to_s] || configured[flag.to_sym] ||
111
+ t("permission_forge.ui.flags.#{flag}", default: flag.to_s)
112
+ end
113
+
114
+ def pf_flag_phrase(flag)
115
+ t("permission_forge.ui.conditions.flags.#{flag}",
116
+ default: pf_ui('conditions.flag_generic', flag: pf_flag_label(flag)))
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,134 @@
1
+ module PermissionForge
2
+ # Labels of stored names in the page's language: a field, column or
3
+ # association name (Admin::FieldLabel), a model or Resource
4
+ # (Admin::ModelLabel), an action (Admin::ActionLabel) — the host's own
5
+ # translation when it has one, else the label an admin wrote, else (on an
6
+ # English page) Rails' English, else the name verbatim, drawn in <code>
7
+ # — and the names of sets, groups and rules (Admin::RecordName), what a
8
+ # form pre-fills (Admin::Prefill). Never Rails' English inflections on a
9
+ # page in another language (release_readiness/inflection_guard_test.rb).
10
+ #
11
+ # @api private
12
+ module LabelsHelper
13
+ # --- labels of stored names --------------------------------------------------
14
+
15
+ # The label of a stored field, column or association name
16
+ # (Admin::FieldLabel): the host's translation, else the row's label,
17
+ # else the name verbatim — then drawn in <code>, as a stored name is.
18
+ def pf_field_label(model, name, row: nil)
19
+ label = pf_field_label_for(model, name, row: row)
20
+ label.verbatim? ? tag.code(label.text, class: 'pf-mono-key') : label.text
21
+ end
22
+
23
+ # Text of the host (a label, a description, a stored value) drawn as it
24
+ # is, marked so the locale-leak test tells it from the gem's own words.
25
+ #
26
+ # @return [ActiveSupport::SafeBuffer]
27
+ def pf_host_data(text, tag_name = :span)
28
+ content_tag(tag_name, text, data: { pf_host_data: true })
29
+ end
30
+
31
+ # The same label as plain text (an attribute, a title, an option).
32
+ def pf_field_label_text(model, name, row: nil)
33
+ pf_field_label_for(model, name, row: row).text
34
+ end
35
+
36
+ # @return [Admin::FieldLabel::Label]
37
+ def pf_field_label_for(model, name, row: nil)
38
+ PermissionForge::Admin::FieldLabel.for(model, name, row: row)
39
+ end
40
+
41
+ # The label of an action (Admin::ActionLabel): the row's label, else the
42
+ # key verbatim, as text.
43
+ def pf_action_label_text(action, key = action&.name)
44
+ PermissionForge::Admin::ActionLabel.for(action, key).text
45
+ end
46
+
47
+ # The plural label of a model (Admin::ModelLabel): the host's plural
48
+ # translation, else "<label> records" in the page's language.
49
+ def pf_resource_plural(name)
50
+ PermissionForge::Admin::ModelLabel.plural(name)
51
+ end
52
+
53
+ # The label of a Resource or model name (Admin::ModelLabel): the row's
54
+ # label, else the host's translation, else the constant's name.
55
+ def pf_resource_label(resource_or_name)
56
+ PermissionForge::Admin::ModelLabel.for(resource_or_name).text
57
+ end
58
+
59
+ # The name of a Permission Set, group or rule (Admin::RecordName): as
60
+ # stored, except an install-default English name, named in the page's
61
+ # language.
62
+ def pf_record_name(record)
63
+ PermissionForge::Admin::RecordName.for(record)
64
+ end
65
+
66
+ # A stored name of `kind` without its record (a list a service answered,
67
+ # an event's label): Admin::RecordName.text.
68
+ def pf_record_name_text(name, kind)
69
+ PermissionForge::Admin::RecordName.text(name, kind)
70
+ end
71
+
72
+ # What a form's text field shows for a stored attribute (Admin::Prefill):
73
+ # never a column default of a new record, nor text the gem generated.
74
+ def pf_prefill(record, attribute)
75
+ record ? PermissionForge::Admin::Prefill.text(record, attribute) : ''
76
+ end
77
+
78
+ # Whether a required text field may be sent blank: the stored value is
79
+ # generated text the form shows as blank (#pf_prefill), and a blank
80
+ # posted over it is no change (Admin::Prefill.posted). Such a field
81
+ # drops `required`, so a page saves with and without JavaScript alike.
82
+ def pf_prefill_optional?(record, attribute)
83
+ return false unless record&.persisted?
84
+
85
+ record.public_send(attribute).to_s.present? && pf_prefill(record, attribute).empty?
86
+ end
87
+
88
+ # A Permission Set named on a row about one of its Resources: "<set> ·
89
+ # <Model>" when the set holds more than one Resource of `chain` (a
90
+ # single-table-inheritance base and its subclass: Admin::ChainEntries),
91
+ # the model marked as host data; the set's name alone otherwise (round 7,
92
+ # B#35).
93
+ #
94
+ # @param set [PermissionSet]
95
+ # @param resource_name [String] the Resource the row is about
96
+ # @param chain [Array<String>] the Resource names of the chain the list is about
97
+ def pf_set_entry_name(set, resource_name, chain)
98
+ name = pf_record_name(set)
99
+ return name unless PermissionForge::Admin::ChainEntries.several?(set, chain)
100
+
101
+ safe_join([name, ' · ', pf_host_data(pf_resource_label(resource_name))])
102
+ end
103
+
104
+ # #pf_set_entry_name on a row about records of `leaf_name` (a person's
105
+ # effective access): the chain is the leaf's ResourceChain; `entry_name`
106
+ # the Resource the row's set acts through (an STI base whose entry
107
+ # decides, or the leaf itself).
108
+ def pf_chain_set_name(set, leaf_name, entry_name = leaf_name)
109
+ pf_set_entry_name(set, entry_name, pf_chain_names(leaf_name))
110
+ end
111
+
112
+ # #pf_chain_set_name for an EffectiveAccess::FieldSource (a set that
113
+ # grants or mutes a field): the entry named is the Resource whose row
114
+ # the source credits (FieldSource#resource_name) — the deciding entry's
115
+ # for a grant, the muting row's for a mute. The row, the Why panel and
116
+ # "Explain for" all name a field's sets through here (B#38).
117
+ def pf_field_source_set_name(source, leaf_name)
118
+ pf_chain_set_name(source.permission_set, leaf_name, source.resource_name || leaf_name)
119
+ end
120
+
121
+ # #pf_chain_set_name for a Muting Permission Set that blocks a flag or an
122
+ # action (an EffectiveAccess::Outcome): the entry named is the Resource
123
+ # whose row blocks it (Outcome#muted_on).
124
+ def pf_muting_set_name(set, outcome, leaf_name)
125
+ pf_chain_set_name(set, leaf_name, outcome.muted_on.fetch(set, leaf_name))
126
+ end
127
+
128
+ # The Resource names of the leaf's ResourceChain (memoized per page).
129
+ def pf_chain_names(leaf_name)
130
+ @pf_chains ||= {}
131
+ @pf_chains[leaf_name.to_s] ||= PermissionForge::ResourceChain.names_for(leaf_name).map(&:to_s)
132
+ end
133
+ end
134
+ end
@@ -9,7 +9,7 @@ module PermissionForge
9
9
  end
10
10
 
11
11
  def pf_person_name(user)
12
- user ? pf_person(user).name : pf_ui('audit.system')
12
+ user ? pf_person(user).name : PermissionForge::Admin::ActorLabel.system
13
13
  end
14
14
 
15
15
  def pf_avatar(user, size: nil)
@@ -30,13 +30,16 @@ module PermissionForge
30
30
 
31
31
  # --- assignments --------------------------------------------------------
32
32
 
33
- # [text, css tone class] for an assignment end date.
33
+ # [text, css tone class] for an assignment end date. "Ended" and "ends
34
+ # in" are PermissionSetUser's (.ended?, ENDING_SOON_DAYS — the window of
35
+ # Home's "ending soon" card and the People page filter).
34
36
  def pf_expiry(expires_at)
35
37
  days = PermissionForge::Admin::ExpiryPresets.days_left(expires_at)
38
+ assignments = PermissionForge::PermissionSetUser
36
39
  return [pf_ui('expiry.none'), 'pf-text-2'] if days.nil?
37
- return [pf_ui('expiry.expired', date: pf_date(expires_at, year: false)), 'pf-text-3'] if expires_at < Time.current
40
+ return [pf_ui('expiry.expired', date: pf_date(expires_at, year: false)), 'pf-text-3'] if assignments.ended?(expires_at)
38
41
  return [pf_ui('expiry.today'), 'pf-text-warn'] if days.zero?
39
- return [pf_ui('expiry.ends_in', count: days, date: pf_date(expires_at, year: false)), 'pf-text-warn'] if days <= 7
42
+ return [pf_ui('expiry.ends_in', count: days, date: pf_date(expires_at, year: false)), 'pf-text-warn'] if days <= assignments::ENDING_SOON_DAYS
40
43
 
41
44
  [pf_ui('expiry.until', date: pf_date(expires_at, year: false)), 'pf-text-2']
42
45
  end
@@ -91,22 +94,11 @@ module PermissionForge
91
94
  @pf_reviewers[id.to_s] = PermissionForge.configuration.user_model.find_by(id: id)
92
95
  end
93
96
 
94
- # Who granted an assignment: the actor of its assignment.granted event
95
- # when the audit log has one (an HTTP Basic login or an API client stamps
96
- # no created_by), else created_by. `granters` is an Admin::LastChanges
97
- # over the assignments of the page (events: %w[assignment.granted]).
98
- def pf_granted_by(assignment, granters = nil)
99
- event = granters && granters[assignment]
100
- event ? pf_audit_actor_name(event) : pf_person_name(assignment.try(:created_by))
101
- end
102
-
103
- def pf_granters(assignments)
104
- PermissionForge::Admin::LastChanges.new(assignments, events: %w[assignment.granted])
105
- end
106
-
97
+ # Due for review: PermissionSetUser#stale? at the admin's threshold
98
+ # (Admin::AttentionItems.stale_days — Home's Stale card and the audit's
99
+ # stale_assignments use the same one, also while the setting is nil).
107
100
  def pf_stale?(assignment)
108
- assignment.expires_at.nil? &&
109
- assignment.last_reviewed_at < PermissionForge.configuration.admin_stale_assignment_days.to_i.days.ago
101
+ assignment.stale?(PermissionForge::Admin::AttentionItems.stale_days)
110
102
  end
111
103
 
112
104
  # Calendar date (not time) for each expiry preset, for labels.
@@ -0,0 +1,73 @@
1
+ module PermissionForge
2
+ # How the admin draws a record rule that matches nothing (Operators::RuleProblem,
3
+ # the one taxonomy the audit's rule check and Home share): a state with a
4
+ # glyph and a word, and why — never the rule's sentence, which would read
5
+ # like a rule that works. SentencesHelper asks here first.
6
+ #
7
+ # @api private
8
+ module RuleProblemsHelper
9
+ # Why the rule matches nothing (Operators::RuleProblem, the definition
10
+ # the audit and Home share), or nil. Asked once per rule object.
11
+ def pf_rule_problem(rule)
12
+ @pf_rule_problems ||= {}.compare_by_identity
13
+ return @pf_rule_problems[rule] if @pf_rule_problems.key?(rule)
14
+
15
+ @pf_rule_problems[rule] = PermissionForge::Operators::RuleProblem.for(rule)
16
+ end
17
+
18
+ # "`stauts` is not a column of Customer": a name that is not there as
19
+ # stored (RuleProblem#verbatim?: `onwer_id`, `ownr`, `regex`, `nosuch`),
20
+ # so the admin recognises the typo — humanized it would read like a real
21
+ # field; a real column or relationship by its label; a value as stored,
22
+ # in code, next to its column's label. The sentence is the problem's
23
+ # (RuleProblem#ui_key). The model may be none this app can load.
24
+ def pf_rule_problem_text(problem)
25
+ model = problem.model
26
+ args = { name: pf_rule_problem_name(problem), model: (pf_resource_label(model.name) if model) }
27
+ args[:value] = content_tag(:code, problem.value, class: 'pf-mono-key') if problem.value
28
+ pf_ui("rules.problems.#{problem.ui_key}", **args)
29
+ end
30
+
31
+ def pf_rule_problem_name(problem)
32
+ return content_tag(:code, problem.name, class: 'pf-mono-key') if problem.verbatim?
33
+
34
+ problem.model ? pf_attribute_label(problem.model, problem.name) : problem.name
35
+ end
36
+
37
+ # "Matches nothing: `stauts` is not a column of Customer"
38
+ def pf_rule_problem_line(problem)
39
+ safe_join([pf_ui('rules.matches_nothing'), ': ', pf_rule_problem_text(problem)])
40
+ end
41
+
42
+ # The rule a one-line summary shows: one that matches nothing first.
43
+ def pf_rule_headline(rules)
44
+ rules.partition { |rule| pf_rule_problem(rule) }.flatten.first
45
+ end
46
+
47
+ # A badge per note on a stored rule (Admin::RuleNotes, most severe
48
+ # first): "⊘ Matches nothing", "⚠ Secret column" — the Rules tab's row.
49
+ def pf_rule_note_badges(rule)
50
+ notes = PermissionForge::Admin::RuleNotes.for(rule)
51
+ safe_join(notes.map do |note|
52
+ word = note.matches_nothing? ? "⊘ #{pf_ui('rules.matches_nothing')}" : "⚠ #{pf_ui("rules.notes.#{note.kind}")}"
53
+ content_tag(:span, pf_badge(word, variant: note.matches_nothing? ? :block : :warn), data: { pf_rule_note: note.kind })
54
+ end, ' ')
55
+ end
56
+
57
+ # ◷ for a rule, ⊘ for one that matches nothing.
58
+ def pf_rule_glyph(rule)
59
+ pf_rule_problem(rule) ? '⊘' : '◷'
60
+ end
61
+
62
+ # [⊘ Matches nothing] Stauts is not a column of Customer `stauts equal active`
63
+ def pf_rule_problem_sentence(rule, problem)
64
+ operator = rule.operator
65
+ value = rule.field_value unless PermissionForge::Operators::Validations.valueless?(operator)
66
+ written = [PermissionForge::Operators::RuleProblem.written_field(rule), operator, value]
67
+ content_tag(:p, safe_join([pf_badge("⊘ #{pf_ui('rules.matches_nothing')}", variant: :block),
68
+ content_tag(:span, pf_rule_problem_text(problem)),
69
+ content_tag(:code, written.compact.join(' '), class: 'pf-mono-key')], ' '),
70
+ class: 'pf-sentence')
71
+ end
72
+ end
73
+ end