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
@@ -8,6 +8,7 @@ module PermissionForge
8
8
 
9
9
  include PermissionForge::Concerns::HasUuid
10
10
  include PermissionForge::Concerns::ValidatesColumnLengths
11
+ include PermissionForge::Concerns::ValidatesRequiredColumns
11
12
  include PermissionForge::Concerns::HasAuditTrail
12
13
  include PermissionForge::Concerns::HasTenantScoping
13
14
  include PermissionForge::Concerns::InvalidatesInBatch
@@ -24,7 +25,8 @@ module PermissionForge
24
25
  # a RecordNotUnique 500 (and prevents silent duplicate rows on schemas that
25
26
  # predate the index). BulkPermissionSetAssignment already de-dupes; this
26
27
  # covers the single-assignment controller/API paths.
27
- validates :user_id, uniqueness: { scope: :permission_set_id }
28
+ validates :user_id, uniqueness: { scope: :permission_set_id,
29
+ message: ->(*) { I18n.t('permission_forge.validations.permission_set_user.taken') } }
28
30
 
29
31
  # An integer user_id column would store a UUID or string user key as its
30
32
  # leading digits, handing this assignment to every user who shares them.
@@ -39,8 +41,14 @@ module PermissionForge
39
41
  # (InvalidatesInBatch#note_previous_value).
40
42
  after_save { note_previous_value('user_id') }
41
43
 
44
+ # The window of "ending soon": Home's card, the People page filter and
45
+ # the end-date cell ("ends in N days") all read it.
46
+ ENDING_SOON_DAYS = 7
47
+
42
48
  scope :active_assignments, -> { where(expires_at: nil).or(where(expires_at: Time.current..)) }
43
49
  scope :expired, -> { where.not(expires_at: nil).where(expires_at: ...Time.current) }
50
+ # Assignments that end within `days` from now (today's included, ended ones not).
51
+ scope :ending_within, ->(days = ENDING_SOON_DAYS) { where(expires_at: Time.current..days.days.from_now.end_of_day) }
44
52
 
45
53
  # Permanent assignments last reviewed before `cutoff` — the access
46
54
  # review list and StaleAssignmentAuditor. Reviewed means a "Keep"
@@ -51,9 +59,18 @@ module PermissionForge
51
59
 
52
60
  apply_tenant_scoping! # from the configuration, as the class loads (HasTenantScoping)
53
61
 
62
+ # Whether an end date has passed: the one comparison behind #expired?,
63
+ # the `expired` scope and the admin's end-date cell.
64
+ #
65
+ # @param expires_at [Time, nil]
66
+ # @return [Boolean]
67
+ def self.ended?(expires_at)
68
+ expires_at.present? && expires_at < Time.current
69
+ end
70
+
54
71
  # The review columns exist (bin/rails generate permission_forge:upgrade).
55
72
  def self.review_supported?
56
- column_names.include?('reviewed_at')
73
+ PermissionForge::SchemaRead.column?(self, 'reviewed_at', ignored: false)
57
74
  end
58
75
 
59
76
  # Arel for "when this assignment was last reviewed" (see stale_before).
@@ -97,7 +114,25 @@ module PermissionForge
97
114
  end
98
115
 
99
116
  def expired?
100
- expires_at.present? && expires_at < Time.current
117
+ self.class.ended?(expires_at)
118
+ end
119
+
120
+ # Whether the assignment ends within `days` (see .ending_within).
121
+ #
122
+ # @param days [Integer]
123
+ # @return [Boolean]
124
+ def ending_soon?(days = ENDING_SOON_DAYS)
125
+ expires_at.present? && !expired? && expires_at <= days.days.from_now.end_of_day
126
+ end
127
+
128
+ # Whether a permanent assignment is due for review: last reviewed more
129
+ # than `days` ago (see .stale_before; Admin::AttentionItems.stale_days
130
+ # is the threshold the admin uses).
131
+ #
132
+ # @param days [Integer]
133
+ # @return [Boolean]
134
+ def stale?(days)
135
+ expires_at.nil? && last_reviewed_at < days.days.ago
101
136
  end
102
137
 
103
138
  def active_assignment?
@@ -9,6 +9,9 @@ module PermissionForge
9
9
  include PermissionForge::Concerns::HasActiveScope
10
10
  include PermissionForge::Concerns::HasUuid
11
11
  include PermissionForge::Concerns::ValidatesColumnLengths
12
+ include PermissionForge::Concerns::RequiresName # name present, on create or rename
13
+ include PermissionForge::Concerns::NormalizesLabel # a blank label is stored as nil
14
+ include PermissionForge::Concerns::ValidatesRequiredColumns
12
15
  include PermissionForge::Concerns::HasAuditTrail
13
16
  include PermissionForge::Concerns::HasTenantScoping
14
17
  include PermissionForge::Concerns::HasPermissionSetUsers
@@ -17,6 +20,10 @@ module PermissionForge
17
20
  include PermissionForge::Concerns::InvalidatesManagedActions
18
21
  include PermissionForge::Concerns::RecordsAuditEvents
19
22
 
23
+ # Its name is configuration, not personal data: #inspect shows it even
24
+ # when filter_parameters (the GDPR patterns) hide a `name` (InspectionFilters).
25
+ PermissionForge::InspectionFilters.show_configuration_names(self)
26
+
20
27
  # A Ruby constant path: Resource#name is resolved to a model class.
21
28
  NAME_FORMAT = PermissionForge::RESOURCE_NAME_FORMAT
22
29
 
@@ -40,16 +47,13 @@ module PermissionForge
40
47
  has_many :permission_set_rules, class_name: 'PermissionForge::PermissionSetRule', dependent: :destroy,
41
48
  extend: Concerns::AuditedCollection
42
49
 
43
- validates :name, presence: true
44
- validates :name, uniqueness: { scope: :permission_set_id, message: 'already exists in this permission set' }
50
+ validates :name, uniqueness: { scope: :permission_set_id,
51
+ message: ->(*) { I18n.t('permission_forge.validations.resource.name_taken') } }
45
52
  # Resource#name is resolved to a model class via safe_constantize (e.g. by
46
53
  # ResourceFieldGenerator and MaskedAttributes auto-apply), so constrain it to
47
54
  # a Ruby constant path. Prevents naming a resource after arbitrary strings.
48
- validates :name, format: {
49
- with: NAME_FORMAT,
50
- message: I18n.t('permission_forge.validations.resource.invalid_name',
51
- default: 'must be a valid model class name (e.g. Customer or Admin::Report)')
52
- }, allow_blank: true
55
+ validates :name, format: { with: NAME_FORMAT, message: ->(*) { I18n.t('permission_forge.validations.resource.invalid_name') } },
56
+ allow_blank: true
53
57
 
54
58
  validates_dependency :edit_access, requires: :read_access
55
59
  validates_dependency :delete_access, requires: :read_access
@@ -72,7 +76,9 @@ module PermissionForge
72
76
  view_all || modify_all
73
77
  end
74
78
 
75
- # Grants full CRUD access + all field access for a resource on a permission set.
79
+ # Grants full CRUD access + read and edit on every field, unmasked (also
80
+ # the columns that look like personal data, which GdprFieldAuditor and the
81
+ # admin's privacy audit still report), for a resource on a permission set.
76
82
  # Intended for seeds, migrations, or console use only.
77
83
  def self.grant_full_access!(permission_set:, resource_name:)
78
84
  PermissionForge::Services::ResourceWithFieldsBuilder.call(
@@ -82,6 +88,10 @@ module PermissionForge
82
88
 
83
89
  private
84
90
 
91
+ # A blank label is stored as no label (NormalizesLabel): the admin names
92
+ # the model from the host's translation (Admin::ModelLabel).
93
+ def generated_label = nil
94
+
85
95
  def note_newly_named_model
86
96
  @pf_newly_named = true if previously_new_record? || saved_change_to_name?
87
97
  end
@@ -9,6 +9,9 @@ module PermissionForge
9
9
  include PermissionForge::Concerns::HasActiveScope
10
10
  include PermissionForge::Concerns::HasUuid
11
11
  include PermissionForge::Concerns::ValidatesColumnLengths
12
+ include PermissionForge::Concerns::RequiresName # name present, on create or rename
13
+ include PermissionForge::Concerns::NormalizesLabel # a blank label is stored as the name
14
+ include PermissionForge::Concerns::ValidatesRequiredColumns
12
15
  include PermissionForge::Concerns::HasAuditTrail
13
16
  include PermissionForge::Concerns::HasTenantScoping
14
17
  include PermissionForge::Concerns::HasPermissionSetUsers
@@ -16,6 +19,10 @@ module PermissionForge
16
19
  include PermissionForge::Concerns::InvalidatesManagedActions
17
20
  include PermissionForge::Concerns::RecordsAuditEvents
18
21
 
22
+ # Its name is configuration, not personal data: #inspect shows it even
23
+ # when filter_parameters (the GDPR patterns) hide a `name` (InspectionFilters).
24
+ PermissionForge::InspectionFilters.show_configuration_names(self)
25
+
19
26
  permission_set_user_lookup :via_resource
20
27
 
21
28
  # optional: as declared since 0.13 (the foreign key holds); stated, so the
@@ -28,10 +35,6 @@ module PermissionForge
28
35
  # instead of a RecordNotUnique 500. Checked when a row is created, renamed
29
36
  # or moved, so duplicates that predate the index stay editable; the
30
37
  # security audit lists them (`duplicate_actions`).
31
- # Checked for a new row and on a rename only: a row whose name was
32
- # blank before 0.14 stays editable in its other attributes.
33
- validates :name, presence: { message: ->(*) { I18n.t('permission_forge.validations.name_blank') } },
34
- if: -> { new_record? || will_save_change_to_name? }
35
38
  validates :name, uniqueness: { scope: :resource_id,
36
39
  message: ->(*) { I18n.t('permission_forge.errors.admin.action_taken') } },
37
40
  if: -> { new_record? || will_save_change_to_name? || will_save_change_to_resource_id? }
@@ -50,9 +53,15 @@ module PermissionForge
50
53
  is_active && resource.in_effect?
51
54
  end
52
55
 
53
- # @return [String] the label, or the humanized name when there is none
56
+ # The action's name as the admin shows it (Admin::ActionLabel): the
57
+ # label an admin wrote; a label the gem generated (blank, the key, the
58
+ # key humanized: GeneratedText.label?) counts as none, and then, on an
59
+ # English page, Rails' English for the key ("Export csv"), else the key
60
+ # as stored ("export_csv").
61
+ #
62
+ # @return [String]
54
63
  def display_name
55
- label.presence || name.humanize
64
+ PermissionForge::Admin::ActionLabel.for(self).text
56
65
  end
57
66
  end
58
67
  end
@@ -8,7 +8,11 @@ module PermissionForge
8
8
 
9
9
  include PermissionForge::Concerns::HasActiveScope
10
10
  include PermissionForge::Concerns::HasUuid
11
+ include PermissionForge::Concerns::KeepsCopiedValues
11
12
  include PermissionForge::Concerns::ValidatesColumnLengths
13
+ include PermissionForge::Concerns::RequiresName # name present, on create or rename
14
+ include PermissionForge::Concerns::NormalizesLabel # a blank label is stored as the name
15
+ include PermissionForge::Concerns::ValidatesRequiredColumns
12
16
  include PermissionForge::Concerns::HasAuditTrail
13
17
  include PermissionForge::Concerns::HasTenantScoping
14
18
  include PermissionForge::Concerns::HasPermissionSetUsers
@@ -16,8 +20,17 @@ module PermissionForge
16
20
  include PermissionForge::Concerns::PermissionsRegenerator
17
21
  include PermissionForge::Concerns::RecordsAuditEvents
18
22
 
23
+ # Its name is configuration, not personal data: #inspect shows it even
24
+ # when filter_parameters (the GDPR patterns) hide a `name` (InspectionFilters).
25
+ PermissionForge::InspectionFilters.show_configuration_names(self)
26
+
19
27
  permission_set_user_lookup :via_resource
20
28
 
29
+ # The install migration's column default for label: a row created
30
+ # without one (the API, the console) is labelled with its name instead
31
+ # (NormalizesLabel), never with this.
32
+ UNTITLED_LABEL = PermissionForge::GeneratedText::UNTITLED_FIELD
33
+
21
34
  # optional: as declared since 0.13 (the foreign key holds); stated, so the
22
35
  # host's belongs_to_required_by_default decides nothing here.
23
36
  belongs_to :resource, class_name: 'PermissionForge::Resource', optional: true
@@ -29,6 +42,69 @@ module PermissionForge
29
42
  .merge(PermissionForge::Resource.active).merge(PermissionForge::PermissionSet.active)
30
43
  }
31
44
 
45
+ # The rows that show a field in the clear — the SQL twin of
46
+ # #exposes_in_clear? (a test pins that they agree). The set's direction
47
+ # is the caller's: `in_effect.merge(PermissionSet.granting)` for the
48
+ # rows that reach someone's hash.
49
+ scope :exposing_in_clear, -> { active.where(read_access: true, mask_access: false) }
50
+
51
+ # The rows in the clear nobody marked "This is intended" in the admin's
52
+ # privacy audit (sensitive_acknowledged_at) — the SQL twin of
53
+ # #exposes_unacknowledged?, what the GDPR audit, the security audit,
54
+ # Home and the boot warning report.
55
+ scope :exposing_unacknowledged, lambda {
56
+ acknowledgement_supported? ? exposing_in_clear.where(sensitive_acknowledged_at: nil) : exposing_in_clear
57
+ }
58
+
59
+ # Whether the table has the column "This is intended" writes (added by
60
+ # the upgrade generator; an older install has none, and nothing is
61
+ # acknowledged there).
62
+ #
63
+ # @return [Boolean]
64
+ def self.acknowledgement_supported?
65
+ PermissionForge::SchemaRead.column?(self, 'sensitive_acknowledged_at', ignored: false)
66
+ rescue StandardError
67
+ false
68
+ end
69
+
70
+ # The rows in effect that mask with a strategy this app does not know
71
+ # (Services::MaskValue.known?): written past the model, or a custom
72
+ # strategy since removed. Masked fully until fixed. The one reading the
73
+ # security audit's unknown_mask_strategies and Home's card both take.
74
+ #
75
+ # @return [Array<ResourceField>] with their Resource and Permission Set loaded
76
+ def self.with_unknown_mask_strategy
77
+ in_effect.where(mask_access: true).includes(resource: :permission_set).order(:resource_id, :id)
78
+ .reject { |field| PermissionForge::Services::MaskValue.known?(field.mask_strategy) }
79
+ end
80
+
81
+ # The one definition of "readable in the clear": an active row that
82
+ # grants read and no mask. On a granting set's row only — a muting row
83
+ # takes access away, so the question does not arise there. The GDPR
84
+ # audit (unmasked_field, cross_set_unmasking), the security audit's
85
+ # personal_data check, Home's privacy card and the boot warning all ask
86
+ # this; a Hidden row (read off) is never in the clear, nor is a row
87
+ # written past the validation with mask on and read off.
88
+ #
89
+ # @return [Boolean]
90
+ def exposes_in_clear?
91
+ is_active && read_access && !mask_access
92
+ end
93
+
94
+ # In the clear and not marked "This is intended": the one definition of
95
+ # a personal-data exposure the four surfaces report (the field must
96
+ # also look like personal data: SensitiveColumns).
97
+ #
98
+ # @return [Boolean]
99
+ def exposes_unacknowledged?
100
+ exposes_in_clear? && !acknowledged_as_intended?
101
+ end
102
+
103
+ # @return [Boolean] an admin marked the field "This is intended"
104
+ def acknowledged_as_intended?
105
+ self.class.acknowledgement_supported? && has_attribute?(:sensitive_acknowledged_at) && sensitive_acknowledged_at.present?
106
+ end
107
+
32
108
  # A duplicate (resource_id, name) row would let a second field entry with
33
109
  # mask_access: false silently unmask the field via FieldPermissionMerger's
34
110
  # AND logic. Enforced by a DB unique index (idx_pf_resource_fields_unique)
@@ -37,26 +113,30 @@ module PermissionForge
37
113
  # collation does, and so the way the index does. On MySQL's default
38
114
  # (case-insensitive) collation "Email" collides with "email" in the index;
39
115
  # a forced case-sensitive check would let it through to a RecordNotUnique.
40
- # Checked for a new row and on a rename only: a row whose name was
41
- # blank before 0.14 stays editable in its other attributes.
42
- validates :name, presence: { message: ->(*) { I18n.t('permission_forge.validations.name_blank') } },
43
- if: -> { new_record? || will_save_change_to_name? }
44
- validates :name, uniqueness: { scope: :resource_id }
116
+ validates :name, uniqueness: { scope: :resource_id, message: ->(*) { I18n.t('permission_forge.validations.taken') } }
45
117
 
46
118
  validates_dependency :edit_access, requires: :read_access
47
119
  validates_dependency :mask_access, requires: :read_access
48
120
 
121
+ # The strategy is checked for a new row and when it, or mask_access,
122
+ # changes (mask_strategy_check_due?): a row whose strategy this app no
123
+ # longer knows — written past the model, or a custom strategy since
124
+ # removed from config.custom_mask_strategies — stays editable in its
125
+ # other attributes. Its values are masked fully until it is fixed
126
+ # (Services::MaskValue); `rake permission_forge:audit` lists it
127
+ # (unknown_mask_strategies), the field drawer flags it.
128
+ # A copy keeps its original's strategy (KeepsCopiedValues): it masks
129
+ # fully, as the original does.
130
+ keeps_copied :mask_access, :mask_strategy
49
131
  validates :mask_strategy, presence: {
50
- message: I18n.t('permission_forge.validations.resource_field.mask_strategy_required',
51
- default: 'must be present when mask access is enabled')
132
+ message: ->(*) { I18n.t('permission_forge.validations.resource_field.mask_strategy_required') }
52
133
  },
53
- if: :mask_access?
134
+ if: :mask_strategy_check_due?
54
135
  validates :mask_strategy, inclusion: {
55
136
  in: ->(_) { PermissionForge::Services::MaskValue.strategies.keys },
56
- message: I18n.t('permission_forge.validations.resource_field.invalid_mask_strategy',
57
- default: 'is not a valid mask strategy')
137
+ message: ->(*) { I18n.t('permission_forge.validations.resource_field.invalid_mask_strategy') }
58
138
  },
59
- if: :mask_access?
139
+ if: :mask_strategy_check_due?
60
140
 
61
141
  apply_tenant_scoping! # from the configuration, as the class loads (HasTenantScoping)
62
142
 
@@ -65,5 +145,11 @@ module PermissionForge
65
145
  def mask_access?
66
146
  mask_access == true
67
147
  end
148
+
149
+ def mask_strategy_check_due?
150
+ return false if copied_unchanged?(:mask_access, :mask_strategy)
151
+
152
+ mask_access? && (new_record? || will_save_change_to_mask_strategy? || will_save_change_to_mask_access?)
153
+ end
68
154
  end
69
155
  end
@@ -1,14 +1,14 @@
1
1
  <% person = pf_person_name(@assignment.user) %>
2
2
  <% set = @assignment.permission_set %>
3
- <% content_for :pf_title, t('.title', set: set.name, name: person) %>
4
- <%= pf_dialog_frame({ title: t('.title', set: set.name, name: person), danger: true,
3
+ <% content_for :pf_title, t('.title', set: pf_record_name(set), name: person) %>
4
+ <%= pf_dialog_frame({ title: t('.title', set: pf_record_name(set), name: person), danger: true,
5
5
  breadcrumb: pf_breadcrumb([t('permission_forge.nav.people'), permission_forge.people_path], [person, permission_forge.person_path(@assignment.user_id)], t('.crumb')) }) do %>
6
6
  <p class="pf-text-2">
7
7
  <% first = pf_person(@assignment.user).first_name %>
8
8
  <% if set.is_muting %>
9
- <%= t('.body_muting', name: first, set: set.name) %>
9
+ <%= t('.body_muting', name: first, set: pf_record_name(set)) %>
10
10
  <% elsif @lost.any? %>
11
- <%= t('.body', name: first, resources: @lost.to_sentence) %>
11
+ <%= t('.body', name: first, resources: pf_to_sentence(@lost)) %>
12
12
  <% else %>
13
13
  <%= t('.body_nothing', name: first) %>
14
14
  <% end %>
@@ -1,7 +1,7 @@
1
1
  <% content_for :pf_title, t('.title') %>
2
2
  <% errors = @errors || {} %>
3
3
  <% title = if @person then t('.title_person', name: pf_person_name(@person))
4
- elsif @preset_set then t('.title_set', set: @preset_set.name)
4
+ elsif @preset_set then t('.title_set', set: pf_record_name(@preset_set))
5
5
  else t('.title') end %>
6
6
  <% close_path = pf_back_or(@person ? permission_forge.person_path(@person) : permission_forge.people_path) %>
7
7
  <%= form_with url: permission_forge.assignments_path, method: :post, id: 'pf-assign-form' do %>
@@ -11,7 +11,7 @@
11
11
  <% if errors.any? %>
12
12
  <% errors.each_value do |msg| %><span class="pf-error" role="alert"><%= msg %></span> <% end %>
13
13
  <% elsif @person && @chosen_sets.any? %>
14
- <%= t('.summary', name: pf_person_name(@person), sets: @chosen_sets.map(&:name).to_sentence,
14
+ <%= t('.summary', name: pf_person_name(@person), sets: pf_to_sentence(@chosen_sets.map(&:name)),
15
15
  until: (@expiry == 'never' ? t('.forever') : t('.until', date: pf_expiry_preset_date(@expiry) || @custom_date))) %>
16
16
  <% else %>
17
17
  <%= t('.applies_now') %>
@@ -63,16 +63,20 @@
63
63
  <% end %>
64
64
  <% @sets.each do |set| %>
65
65
  <% has = @existing_ids.include?(set.id) %>
66
+ <%# A locked set that would widen access by taking a holder is shown, but cannot be picked. %>
67
+ <% locked = !has && @locked_ids.include?(set.id) %>
66
68
  <label class="pf-option">
67
- <input type="checkbox" name="set_ids[]" value="<%= set.id %>" <%= 'checked' if @selected_ids.include?(set.id) && !has %> <%= 'disabled' if has %>>
69
+ <input type="checkbox" name="set_ids[]" value="<%= set.id %>" <%= 'checked' if @selected_ids.include?(set.id) && !has && !locked %> <%= 'disabled' if has || locked %>>
68
70
  <span class="pf-option__main">
69
71
  <span class="pf-row pf-row--tight">
70
- <span class="pf-fw-600"><%= set.name %></span>
72
+ <span class="pf-fw-600"><%= pf_record_name(set) %></span>
71
73
  <% if has %><%= pf_badge(t('.already')) %><% end %>
74
+ <% if locked %><%= pf_lock_badge %><% end %>
72
75
  <% if set.is_muting %><%= pf_badge("⊘ #{t('permission_forge.ui.badges.muting')}", variant: :muting) %><% end %>
73
76
  <% unless set.is_active %><%= pf_off_badge %><% end %>
74
77
  </span>
75
- <% if set.description.present? %><span class="pf-fs-12 pf-text-3"><%= set.description %></span><% end %>
78
+ <% if set.description.present? %><span class="pf-fs-12 pf-text-3" data-pf-host-data><%= set.description %></span><% end %>
79
+ <% if locked %><span class="pf-fs-12 pf-text-3"><%= t('.locked_hint') %></span><% end %>
76
80
  </span>
77
81
  <span class="pf-option__aside"><%= pf_people_text(@people_counts.fetch(set.id, 0)) %></span>
78
82
  </label>
@@ -83,7 +87,7 @@
83
87
  <% if muting.any? %>
84
88
  <div class="pf-banner pf-banner--muting">
85
89
  <span class="pf-banner__glyph" aria-hidden="true">⊘</span>
86
- <div class="pf-banner__body"><span><%= t('.muting_notice', sets: muting.map(&:name).to_sentence, name: @person ? pf_person(@person).first_name : t('.this_person')) %></span></div>
90
+ <div class="pf-banner__body"><span><%= t('.muting_notice', sets: pf_to_sentence(muting.map(&:name)), name: @person ? pf_person(@person).first_name : t('.this_person')) %></span></div>
87
91
  </div>
88
92
  <% end %>
89
93
  </section>
@@ -92,7 +92,7 @@
92
92
  <select id="pf-audit-set" class="pf-select pf-select--sm" name="permission_set" <%= invalid.('permission_set') %>>
93
93
  <option value=""><%= t('.filters.any_set') %></option>
94
94
  <% sets.each do |id, name| %>
95
- <option value="<%= id %>" <%= 'selected' if value.('permission_set') == id.to_s %>><%= name %></option>
95
+ <option value="<%= id %>" <%= 'selected' if value.('permission_set') == id.to_s %>><%= pf_record_name_text(name, :permission_set) %></option>
96
96
  <% end %>
97
97
  <% if value.('permission_set').present? && sets.none? { |id, _| id.to_s == value.('permission_set') } %>
98
98
  <option value="<%= value.('permission_set') %>" selected><%= t('.filters.deleted_set', id: value.('permission_set')) %></option>
@@ -21,17 +21,24 @@
21
21
  <dl class="pf-card__body pf-kv">
22
22
  <dt><%= t('.actor') %></dt>
23
23
  <dd>
24
+ <% unknown = pf_audit_unknown_actor?(event) %>
24
25
  <span class="pf-fw-600"><%= pf_audit_actor_name(event) %></span>
25
- <span class="pf-text-3">· <%= pf_audit_actor_type_label(event.actor_type) %><% if d.actor_id? %> · <%= t('.id', id: event.actor_id) %><% end %></span>
26
+ <%# An unknown actor's name is its type already: "Unknown · Unknown" said nothing twice. %>
27
+ <% unless unknown %>
28
+ <span class="pf-text-3">· <%= pf_audit_actor_type_label(event.actor_type) %><% if d.actor_id? %> · <%= t('.id', id: event.actor_id) %><% end %></span>
29
+ <% end %>
30
+ <%# The actor line above already reads "Unknown": the explanation does not start with it again. %>
31
+ <% if unknown %><p class="pf-fs-13 pf-text-2"><%= pf_audit_unknown_actor_hint %></p><% end %>
26
32
  <div class="pf-row pf-row--tight pf-fs-12">
27
- <%= link_to t('.all_by_actor'), pf_audit_log_path(actor_type: event.actor_type, actor_id: event.actor_id) %>
33
+ <%# With no actor named, the link lists every event without one, and says so. %>
34
+ <%= link_to (unknown ? t('.all_unknown_actor') : t('.all_by_actor')), pf_audit_log_path(actor_type: event.actor_type, actor_id: event.actor_id) %>
28
35
  <% if d.actor_user %>· <%= link_to t('.open_person'), permission_forge.person_path(d.actor_user) %><% end %>
29
36
  </div>
30
37
  </dd>
31
38
  <dt><%= t('.source') %></dt>
32
39
  <dd><%= pf_audit_source_label(event.source) %> <span class="pf-mono-key"><%= event.source %></span></dd>
33
40
  <dt><%= t('.reason') %></dt>
34
- <dd><%= event.reason.presence || t('.no_reason') %></dd>
41
+ <dd><%= pf_audit_reason(event) || t('.no_reason') %></dd>
35
42
  <dt><%= t('.request_id') %></dt>
36
43
  <dd class="pf-audit-code"><%= event.request_id.presence || t('permission_forge.audit_log.no_value') %></dd>
37
44
  <dt><%= t('.ip') %></dt>
@@ -90,7 +97,7 @@
90
97
  </div>
91
98
 
92
99
  <% if d.changes? %>
93
- <section class="pf-card pf-scroll-x" aria-labelledby="pf-event-changes">
100
+ <%= pf_scroll_frame(tag: :section, 'aria-labelledby': 'pf-event-changes', class: 'pf-card') do %>
94
101
  <div class="pf-card__header">
95
102
  <h2 class="pf-h2" id="pf-event-changes"><%= t('.changes') %></h2>
96
103
  <span class="pf-card__desc"><%= t('.changes_desc', count: d.changes_count) %></span>
@@ -107,11 +114,11 @@
107
114
  <% end %>
108
115
  </tbody>
109
116
  </table>
110
- </section>
117
+ <% end %>
111
118
  <% end %>
112
119
 
113
120
  <% if d.snapshot? %>
114
- <section class="pf-card pf-scroll-x" aria-labelledby="pf-event-snapshot">
121
+ <%= pf_scroll_frame(tag: :section, 'aria-labelledby': 'pf-event-snapshot', class: 'pf-card') do %>
115
122
  <div class="pf-card__header">
116
123
  <h2 class="pf-h2" id="pf-event-snapshot"><%= t('.snapshot') %></h2>
117
124
  <span class="pf-card__desc"><%= d.snapshot_of_removed? ? t('.snapshot_destroyed') : t('.snapshot_desc') %></span>
@@ -124,11 +131,11 @@
124
131
  <% end %>
125
132
  </tbody>
126
133
  </table>
127
- </section>
134
+ <% end %>
128
135
  <% end %>
129
136
 
130
137
  <% if d.other? %>
131
- <section class="pf-card pf-scroll-x" aria-labelledby="pf-event-data">
138
+ <%= pf_scroll_frame(tag: :section, 'aria-labelledby': 'pf-event-data', class: 'pf-card') do %>
132
139
  <div class="pf-card__header"><h2 class="pf-h2" id="pf-event-data"><%= t('.data') %></h2></div>
133
140
  <table class="pf-audit-table">
134
141
  <thead><tr><th scope="col"><%= t('.key') %></th><th scope="col"><%= t('.value') %></th></tr></thead>
@@ -138,7 +145,7 @@
138
145
  <% end %>
139
146
  </tbody>
140
147
  </table>
141
- </section>
148
+ <% end %>
142
149
  <% end %>
143
150
 
144
151
  <% if d.no_data? %>
@@ -1,8 +1,8 @@
1
1
  <% set = @permission_set %>
2
2
  <% back = permission_forge.permission_set_path(set, tab: 'assignments') %>
3
- <% content_for :pf_title, t('.title', count: @people.size, set: set.name) %>
4
- <%= pf_dialog_frame({ title: t('.title', count: @people.size, set: set.name), danger: true,
5
- breadcrumb: pf_breadcrumb([t('permission_forge.nav.permission_sets'), permission_forge.permission_sets_path], [set.name, back], t('.crumb')) }) do %>
3
+ <% content_for :pf_title, t('.title', count: @people.size, set: pf_record_name(set)) %>
4
+ <%= pf_dialog_frame({ title: t('.title', count: @people.size, set: pf_record_name(set)), danger: true,
5
+ breadcrumb: pf_breadcrumb([t('permission_forge.nav.permission_sets'), permission_forge.permission_sets_path], [pf_record_name(set), back], t('.crumb')) }) do %>
6
6
  <% if @people.empty? %>
7
7
  <p class="pf-text-2"><%= t('.none') %></p>
8
8
  <div class="pf-actions pf-row--right"><%= link_to t('permission_forge.ui.close'), back, class: 'pf-btn', data: { pf_close: true } %></div>
@@ -1,5 +1,5 @@
1
1
  <% set = @permission_set %>
2
- <% content_for :pf_title, t('.title', set: set.name) %>
2
+ <% content_for :pf_title, t('.title', set: pf_record_name(set)) %>
3
3
  <% close_path = permission_forge.permission_set_path(set, tab: 'assignments') %>
4
4
  <%= form_with url: permission_forge.permission_set_bulk_assignment_path(set), method: :post do %>
5
5
  <% footer = capture do %>
@@ -15,8 +15,8 @@
15
15
  <%= @result ? t('.assign_count', count: @result.to_assign.size) : t('.assign') %>
16
16
  </button>
17
17
  <% end %>
18
- <%= pf_sheet_frame({ title: t('.title', set: set.name), close_path: close_path, footer: footer,
19
- breadcrumb: pf_breadcrumb([t('permission_forge.nav.permission_sets'), permission_forge.permission_sets_path], [set.name, close_path], t('.crumb')) }) do %>
18
+ <%= pf_sheet_frame({ title: t('.title', set: pf_record_name(set)), close_path: close_path, footer: footer,
19
+ breadcrumb: pf_breadcrumb([t('permission_forge.nav.permission_sets'), permission_forge.permission_sets_path], [pf_record_name(set), close_path], t('.crumb')) }) do %>
20
20
  <div class="pf-field">
21
21
  <label class="pf-label" for="pf-bulk-text"><%= t('.paste') %></label>
22
22
  <textarea id="pf-bulk-text" name="identifiers" rows="5" class="pf-textarea pf-textarea--mono" placeholder="<%= t('.paste_placeholder') %>"><%= @text %></textarea>
@@ -7,7 +7,7 @@
7
7
  <select id="pf-lookup-resource" name="resource" class="pf-select" data-pf-autosubmit>
8
8
  <option value=""><%= t('.pick_resource') %></option>
9
9
  <% @resource_names.each do |name| %>
10
- <option value="<%= name %>" <%= 'selected' if name == @resource_name %>><%= pf_resource_label(name) %> (<%= name %>)</option>
10
+ <option value="<%= name %>" <%= 'selected' if name == @resource_name %> data-pf-host-data><%= pf_resource_label(name) %> (<%= name %>)</option>
11
11
  <% end %>
12
12
  </select>
13
13
  </div>
@@ -16,7 +16,7 @@
16
16
  <select id="pf-lookup-field" name="field" class="pf-select" data-pf-autosubmit <%= 'disabled' unless @resource_name %>>
17
17
  <option value=""><%= t('.pick_field') %></option>
18
18
  <% @field_options.each do |name, label| %>
19
- <option value="<%= name %>" <%= 'selected' if name == @field %>><%= label %> (<%= name %>)</option>
19
+ <option value="<%= name %>" <%= 'selected' if name == @field %> data-pf-host-data><%= label %> (<%= name %>)</option>
20
20
  <% end %>
21
21
  </select>
22
22
  </div>
@@ -0,0 +1,35 @@
1
+ <% items = @attention.broken_rules %>
2
+ <section class="pf-card" aria-labelledby="pf-card-broken-rules">
3
+ <div class="pf-card__header">
4
+ <div class="pf-stack pf-stack--tight pf-grow">
5
+ <div class="pf-row">
6
+ <span class="pf-text-block" aria-hidden="true">⊘</span>
7
+ <h2 class="pf-h2" id="pf-card-broken-rules"><%= t('.title') %></h2>
8
+ <span class="pf-count-pill pf-count-pill--warn"><%= items.size %></span>
9
+ </div>
10
+ <p class="pf-card__desc"><%= t('.description') %></p>
11
+ </div>
12
+ </div>
13
+ <% items.each do |item| %>
14
+ <% set = item.permission_set %>
15
+ <% resource = item.resource %>
16
+ <div class="pf-card__row pf-card__row--top">
17
+ <div class="pf-stack pf-stack--tight pf-grow">
18
+ <div class="pf-row pf-row--tight">
19
+ <strong><%= t('.rule', rule: pf_record_name(item.rule), resource: pf_resource_label(resource), set: pf_record_name(set)) %></strong>
20
+ <%= pf_lock_badge if item.locked? %>
21
+ </div>
22
+ <span class="pf-fs-13 pf-text-block"><%= pf_rule_problem_line(item.problem) %></span>
23
+ <span class="pf-fs-13 pf-text-2"><%= t('.people', count: item.people_count) %></span>
24
+ <%# A locked rule cannot be fixed here (its page offers only a copy): say where it can be. %>
25
+ <% if item.locked? %><span class="pf-fs-13 pf-text-2"><%= t('.locked_fix_html', flag: content_tag(:code, 'is_editable: false')) %></span><% end %>
26
+ </div>
27
+ <% if item.locked? %>
28
+ <%= link_to t('.open'), permission_forge.permission_set_resource_path(set, resource.name, tab: 'rules'), class: 'pf-btn pf-btn--ghost' %>
29
+ <% else %>
30
+ <%= pf_modal_link t('.fix'), permission_forge.edit_permission_set_resource_rule_path(set, resource.name, item.rule),
31
+ kind: 'drawer', class: 'pf-btn pf-btn--sm' %>
32
+ <% end %>
33
+ </div>
34
+ <% end %>
35
+ </section>
@@ -0,0 +1,28 @@
1
+ <% items = @attention.broken_conditions %>
2
+ <section class="pf-card" aria-labelledby="pf-card-conditions">
3
+ <div class="pf-card__header">
4
+ <div class="pf-stack pf-stack--tight pf-grow">
5
+ <div class="pf-row">
6
+ <span class="pf-text-warn" aria-hidden="true">⚠</span>
7
+ <h2 class="pf-h2" id="pf-card-conditions"><%= t('.title') %></h2>
8
+ <span class="pf-count-pill pf-count-pill--warn"><%= items.size %></span>
9
+ </div>
10
+ <p class="pf-card__desc"><%= t('.description') %></p>
11
+ </div>
12
+ </div>
13
+ <% items.each do |item| %>
14
+ <% set = item.permission_set %>
15
+ <div class="pf-card__row pf-card__row--top">
16
+ <div class="pf-stack pf-stack--tight pf-grow">
17
+ <div class="pf-row pf-row--tight">
18
+ <strong><%= pf_record_name(set) %></strong>
19
+ <%= pf_set_badges(set) %>
20
+ </div>
21
+ <%# The effect as the set's Conditions tab words it (pf_condition_effect). %>
22
+ <span class="pf-fs-13 pf-text-2"><%= pf_condition_effect(set) %></span>
23
+ <span class="pf-fs-13 pf-text-2"><%= t('.people', count: item.people_count) %></span>
24
+ </div>
25
+ <%= link_to t('.open'), permission_forge.permission_set_path(set, tab: 'conditions'), class: 'pf-btn pf-btn--sm' %>
26
+ </div>
27
+ <% end %>
28
+ </section>