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
@@ -17,18 +17,86 @@ module PermissionForge
17
17
  CRUD = %i[read_access create_access edit_access delete_access].freeze
18
18
  RESOURCE_FLAGS = (CRUD + %i[view_all modify_all]).freeze
19
19
 
20
- # A permission outcome with the sets that caused it.
21
- Outcome = Struct.new(:state, :granted_by, :blocked_by, keyword_init: true) do
20
+ # A permission outcome with the sets that caused it. `muted_on`: each
21
+ # blocking set => the Resource whose row blocks (a single-table-
22
+ # inheritance base or the resource itself; the row nearest the
23
+ # resource when the set has several), so a reason names the Resource
24
+ # the set acts through (LabelsHelper#pf_chain_set_name).
25
+ Outcome = Struct.new(:state, :granted_by, :blocked_by, :muted_on, keyword_init: true) do
26
+ # The outcome of the granting sets and the blocking ones (set => Resource).
27
+ def self.of(granted_by, muted_on)
28
+ state = if granted_by.empty? then :none
29
+ elsif muted_on.any? then :blocked
30
+ else :granted
31
+ end
32
+ new(state: state, granted_by: granted_by, blocked_by: muted_on.keys, muted_on: muted_on)
33
+ end
34
+
22
35
  def granted? = state == :granted
23
36
  def blocked? = state == :blocked
24
37
  end
25
38
 
39
+ # What one Permission Set does to a field: its most open level (a
40
+ # granting set: masked, read or edit, with that row's mask strategy) or
41
+ # what it takes away (a muting set: mask, hide or no_edit) —
42
+ # Admin::FieldLevel names. `resource_name`: the Resource whose row
43
+ # does it — the deciding entry's for a grant, the muting row's (an STI
44
+ # base's or the resource's own) for a mute (field test round 8, B#38).
45
+ FieldSource = Struct.new(:permission_set, :level, :mask_strategy, :resource_name, keyword_init: true)
46
+
26
47
  # `implicit` — no set configures the field for this outcome: it is a
27
48
  # declared virtual field on the implicit read + edit grant
28
49
  # (extra_fields_default_access = :grant).
50
+ # `grants` / `mutes` — the FieldSources behind granted_by / blocked_by, same order:
51
+ # the sets credited with what they did to the entry that decides.
52
+ # `decided_by` — for a single-table-inheritance subclass, the base
53
+ # Resource whose entry decides the field (ResourceChain: the strictest
54
+ # entry along the chain); nil when the resource's own entry does.
55
+ # `unreadable` — that entry does not grant Read, so it hides every field.
56
+ # The level a field shows when a mute decides it (FieldSource#level of a
57
+ # mute => Admin::FieldLevel granting level): hide => Hidden, mask =>
58
+ # Masked, no_edit => Read-only.
59
+ MUTED_LEVELS = { 'hide' => 'hidden', 'mask' => 'masked', 'no_edit' => 'read' }.freeze
60
+
29
61
  FieldOutcome = Struct.new(:name, :level, :mask_strategy, :granted_by, :blocked_by, :blocked_level,
30
- :capped, :unconfigured, :virtual, :variant_of, :implicit, keyword_init: true) do
62
+ :capped, :unconfigured, :virtual, :variant_of, :implicit, :grants, :mutes,
63
+ :decided_by, :unreadable, keyword_init: true) do
31
64
  def visible? = level != 'hidden'
65
+ # Hidden because a Muting Permission Set takes it away, not because no set grants it.
66
+ def hidden_by_muting? = level == 'hidden' && blocked_by.any?
67
+
68
+ # The mutes that decide the field's level: the strongest mute's level
69
+ # (blocked_level) when it is the level the field shows
70
+ # (MUTED_LEVELS), and every set credited at it; none otherwise — a
71
+ # mute that took editing away from a field a grant shows masked
72
+ # changed the entry but does not decide what it shows. The row and
73
+ # "Explain for" name these sets (review of round 8, D1/D2).
74
+ #
75
+ # @return [Array<FieldSource>]
76
+ def deciding_mutes
77
+ return [] unless blocked_level && MUTED_LEVELS[blocked_level] == level
78
+
79
+ mutes.select { |source| source.level == blocked_level }
80
+ end
81
+
82
+ # Why the field is at its level: the one decision a person's
83
+ # Effective access row, its Why panel and the field drawer's
84
+ # "Explain for" all word (field test round 8, B#39) —
85
+ # :muted (#deciding_mutes: a Muting Permission Set decides), :unconfigured, :granted,
86
+ # :implicit (a virtual field's default), :unreadable (the deciding
87
+ # entry does not grant Read), :inherited (an STI base's entry
88
+ # decides and shows it to nobody) or :none (no set shows it).
89
+ #
90
+ # @return [Symbol]
91
+ def reason
92
+ return :muted if deciding_mutes.any?
93
+ return :unconfigured if unconfigured
94
+ return :granted if granted_by.any?
95
+ return :implicit if implicit
96
+ return :unreadable if unreadable
97
+
98
+ decided_by ? :inherited : :none
99
+ end
32
100
  end
33
101
 
34
102
  RuleSource = Struct.new(:rule, :permission_set, keyword_init: true)
@@ -39,8 +107,25 @@ module PermissionForge
39
107
  end
40
108
 
41
109
  # One resource name with its [permission_set, resource] pairs from the
42
- # granting and muting sets, and the resulting resource flag outcomes.
43
- ResourceSources = Struct.new(:name, :grants, :mutes, :flags, keyword_init: true)
110
+ # granting and muting sets, the resulting resource flag outcomes and
111
+ # the record rules that apply (its own entry's and, for a
112
+ # single-table-inheritance subclass, its bases' — ResourceChain.rules).
113
+ ResourceSources = Struct.new(:name, :grants, :mutes, :flags, :rules, keyword_init: true) do
114
+ # View All / Modify All on this entry skips its own rules (ResourceChain.rules).
115
+ def bypassed? = flags[:view_all].granted? || flags[:modify_all].granted?
116
+
117
+ # :all when View All is granted and no rule applies (a base entry's
118
+ # may, past the subclass's own bypass); :rules otherwise.
119
+ def records
120
+ return :none unless flags[:read_access].granted?
121
+
122
+ flags[:view_all].granted? && rules.empty? ? :all : :rules
123
+ end
124
+
125
+ def outcome(fields:, actions:)
126
+ ResourceOutcome.new(name: name, flags: flags, records: records, rules: rules, fields: fields, actions: actions)
127
+ end
128
+ end
44
129
 
45
130
  attr_reader :live_sets, :conditional_sets
46
131
 
@@ -59,17 +144,20 @@ module PermissionForge
59
144
  granting, muting = @live_sets.partition(&:granting?)
60
145
 
61
146
  names = granting.flat_map { |ps| active_resources(ps).map(&:name) }.uniq
62
- names.map { |name| build_resource(sources_for(name, granting, muting)) }
147
+ names.map { |name| outcome_for_name(name, granting, muting) }
63
148
  end
64
149
 
65
150
  private
66
151
 
67
152
  # Same selection as PermissionCacheAccessor#active_non_expired_permission_sets
68
- # minus the request-context filter.
153
+ # minus the request-context filter — a granting set whose condition
154
+ # nobody can judge never passes it, so it is left out
155
+ # (ActivationConditions#never_grants?).
69
156
  def select_live_sets
70
157
  ids = PermissionForge::PermissionSetUser.where(user_id: @user.id).active_assignments.select(:permission_set_id)
71
- sets = PermissionForge::PermissionSet.active.where(id: ids).order(:created_at, :id)
72
- .includes(resources: %i[resource_fields resource_actions permission_set_rules]).to_a
158
+ sets = PermissionForge::PermissionSet.active.where(id: ids).in_hash_order
159
+ .includes(resources: %i[resource_fields resource_actions permission_set_rules])
160
+ .reject(&:never_grants?)
73
161
  # Same row order as the hash builder (a variant tie goes to the older row).
74
162
  sets.each { |permission_set| PermissionForge::Services::PermissionHashBuilder.sort_rows(permission_set) }
75
163
  end
@@ -79,13 +167,26 @@ module PermissionForge
79
167
  end
80
168
 
81
169
  # [permission_set, resource] for every active row of the sets named by
82
- # one of `names`, in set order then row order.
170
+ # one of `names`, in set order, then in the order of `names` (a muting
171
+ # set's row nearest the resource first), then row order.
83
172
  def pairs_for(permission_sets, names)
84
173
  permission_sets.flat_map do |ps|
85
- active_resources(ps).select { |r| names.include?(r.name) }.map { |resource| [ps, resource] }
174
+ ChainEntries.in_chain_order(active_resources(ps), names).map { |resource| [ps, resource] }
86
175
  end
87
176
  end
88
177
 
178
+ # The outcome for one resource name: its sources, the rules along its
179
+ # chain (ResourceChain.names_for), then the fields — composed with the
180
+ # entries of its bases the person has (ResourceChain.fields) — and
181
+ # actions.
182
+ def outcome_for_name(name, granting, muting)
183
+ chain = PermissionForge::ResourceChain.names_for(name).map(&:to_s)
184
+ sources = sources_for(name, granting, muting)
185
+ sources.rules = rules_along(chain, granting, muting)
186
+ bases = chain.drop(1).map { |base| sources_for(base, granting, muting) }.select { |base| base.grants.any? }
187
+ build_resource(sources, bases)
188
+ end
189
+
89
190
  # A granting set's row for the resource; a muting set's rows for the
90
191
  # resource and for the STI bases of the model it names — the engine's
91
192
  # cascade (PermissionHashBuilder#apply_muting_permission_set): a muted
@@ -93,44 +194,45 @@ module PermissionForge
93
194
  def sources_for(name, granting, muting)
94
195
  grants = pairs_for(granting, [name])
95
196
  mutes = pairs_for(muting, PermissionForge::ResourceChain.names_for(name).map(&:to_s))
96
- flags = RESOURCE_FLAGS.index_with { |flag| outcome_for(grants, mutes) { |r| r.public_send(flag) } }
197
+ flags = RESOURCE_FLAGS.index_with { |flag| flag_outcome(grants, mutes, flag) }
97
198
  ResourceSources.new(name: name, grants: grants, mutes: mutes, flags: flags)
98
199
  end
99
200
 
100
- def build_resource(sources)
101
- grants = sources.grants
102
- ResourceOutcome.new(
103
- name: sources.name, flags: sources.flags,
104
- records: records_state(sources.flags),
105
- rules: rules_for(grants),
106
- fields: EffectiveFields.new(sources, configuration: @configuration, catalog: @catalog).call,
107
- actions: action_outcomes(grants, sources.mutes)
108
- )
109
- end
110
-
111
- # Granted by every granting set whose resource matches; muting sets only
112
- # block what some set granted.
113
- def outcome_for(grants, mutes, &)
114
- granted_by = sets_matching(grants, &)
115
- blocked_by = granted_by.any? ? sets_matching(mutes, &) : []
116
- Outcome.new(state: outcome_state(granted_by, blocked_by), granted_by: granted_by, blocked_by: blocked_by)
201
+ # The rules that apply to records of `chain.first` (ResourceChain.rules):
202
+ # of every entry along the chain without a bypass of its own.
203
+ def rules_along(chain, granting, muting)
204
+ chain.flat_map do |name|
205
+ sources = sources_for(name, granting, muting)
206
+ sources.bypassed? ? [] : rules_for(sources.grants)
207
+ end
117
208
  end
118
209
 
119
- # Each set once: a muting set may reach an entry through two rows.
120
- def sets_matching(pairs)
121
- pairs.select { |_, resource| yield resource }.map(&:first).uniq
210
+ # `bases`: the ResourceSources of the base entries the person has
211
+ # (a granting row names them — the engine's entries), nearest first.
212
+ def build_resource(sources, bases)
213
+ fields = EffectiveFields.new(sources, configuration: @configuration, catalog: @catalog, bases: bases).call
214
+ sources.outcome(fields: fields, actions: action_outcomes(sources))
122
215
  end
123
216
 
124
- def outcome_state(granted_by, blocked_by)
125
- return :none if granted_by.empty?
126
-
127
- blocked_by.any? ? :blocked : :granted
217
+ # A resource flag: granted by the granting rows that grant it as the
218
+ # engine reads them (PermissionHashBuilder.granted_flags — edit and
219
+ # delete only with read, Modify All only with View All), blocked by the
220
+ # muting rows that carry it.
221
+ def flag_outcome(grants, mutes, flag)
222
+ outcome_for(grants, mutes) do |resource, granting|
223
+ granting ? PermissionForge::Services::PermissionHashBuilder.granted_flags(resource).fetch(flag) : resource.public_send(flag) == true
224
+ end
128
225
  end
129
226
 
130
- def records_state(flags)
131
- return :none unless flags[:read_access].granted?
132
-
133
- flags[:view_all].granted? ? :all : :rules
227
+ # Granted by every granting set whose resource matches; muting sets only
228
+ # block what some set granted, each set once (a muting set may reach an
229
+ # entry through two rows: its first that blocks is named, pairs_for
230
+ # puts the one nearest the resource first). The block gets the row and
231
+ # whether it is a granting one.
232
+ def outcome_for(grants, mutes)
233
+ granted_by = grants.select { |_, resource| yield resource, true }.map(&:first).uniq
234
+ blocking = granted_by.any? ? mutes.select { |_, resource| yield resource, false } : []
235
+ Outcome.of(granted_by, blocking.group_by(&:first).transform_values { |rows| rows.first.last.name })
134
236
  end
135
237
 
136
238
  def rules_for(grants)
@@ -141,17 +243,14 @@ module PermissionForge
141
243
 
142
244
  # --- Actions ----------------------------------------------------------
143
245
 
144
- def action_outcomes(grants, mutes)
145
- names = grants.flat_map { |_, r| active_actions(r).map(&:name) }.uniq
146
- names.map { |action| [action, outcome_for(grants, mutes) { |r| permitted?(r, action) }] }
147
- end
148
-
149
- def active_actions(resource)
150
- resource.resource_actions.select(&:is_active)
246
+ def action_outcomes(sources)
247
+ grants = sources.grants
248
+ names = grants.flat_map { |_, r| r.resource_actions.select(&:is_active).map(&:name) }.uniq
249
+ names.map { |action| [action, outcome_for(grants, sources.mutes) { |r, _granting| permitted?(r, action) }] }
151
250
  end
152
251
 
153
252
  def permitted?(resource, action)
154
- active_actions(resource).select(&:is_permitted).map(&:name).include?(action)
253
+ resource.resource_actions.any? { |row| row.is_active && row.is_permitted && row.name == action }
155
254
  end
156
255
  end
157
256
  end
@@ -10,6 +10,12 @@ module PermissionForge
10
10
  # Extra fields follow the engine: after every granting set is merged, each
11
11
  # declared name that nothing configured gets the implicit entry
12
12
  # (ExtraFieldDefaults). A row in any set wins, whatever the set order.
13
+ #
14
+ # The explanation comes from the entry that decides (EX): each set is
15
+ # credited with what it did to the final entry (FieldProvenance). A
16
+ # subclass's field is composed along its single-table-inheritance chain
17
+ # as ResourceChain composes it (`bases`, FieldChain): the strictest entry
18
+ # decides, and the outcome says which (FieldOutcome#decided_by).
13
19
  class EffectiveFields
14
20
  FieldOutcome = EffectiveAccess::FieldOutcome
15
21
 
@@ -18,56 +24,91 @@ module PermissionForge
18
24
  UNCONFIGURED_LEVELS = { 'pass_through' => 'read', 'warn' => 'read', 'mask_all' => 'masked' }.freeze
19
25
 
20
26
  # @param sources [EffectiveAccess::ResourceSources]
21
- def initialize(sources, configuration:, catalog:)
27
+ # @param bases [Array<EffectiveAccess::ResourceSources>] the entries the
28
+ # user has for the model's STI bases, nearest first (ResourceChain)
29
+ def initialize(sources, configuration:, catalog:, bases: [])
22
30
  @sources = sources
23
31
  @configuration = configuration
24
32
  @catalog = catalog
25
- # entry: { read_access:, edit_access:, mask_access:, mask_strategy:, sources: [[set, level]], variant_of: }
33
+ @bases = bases
34
+ # entry: { read_access:, edit_access:, mask_access:, mask_strategy:, sources: [[set, level, strategy, resource]],
35
+ # variant_of:, before: (the flags before muting), mutes: [[set, level, resource]] }
36
+ # (resource: the name of the Resource whose row it is)
26
37
  @entries = {}
27
38
  end
28
39
 
29
40
  # @return [Array<EffectiveAccess::FieldOutcome>]
30
41
  def call
42
+ chain = FieldChain.new(chain_tables)
43
+ model = @catalog.find(@sources.name)
44
+ (chain.names + Array(model&.managed_field_names)).uniq.map do |field|
45
+ composed_outcome(chain, field, model.present? && model.virtual_field?(field))
46
+ end
47
+ end
48
+
49
+ # The resource's own field entries, merged and muted as the engine does.
50
+ #
51
+ # @return [Hash{String => Hash}]
52
+ def entries
53
+ return @entries if @merged
54
+
55
+ @merged = true
31
56
  merge_granting_fields
32
57
  apply_muting_fields
58
+ @entries
59
+ end
33
60
 
34
- model = @catalog.find(@sources.name)
35
- (@entries.keys + Array(model&.managed_field_names)).uniq.map do |field|
36
- field_outcome(field, model.present? && model.virtual_field?(field))
61
+ private
62
+
63
+ # The resource's FieldChain::Table, then each base's along its chain.
64
+ def chain_tables
65
+ [FieldChain::Table.new(@sources, entries, nil)] + @bases.map do |base|
66
+ FieldChain::Table.new(base, self.class.new(base, configuration: @configuration, catalog: @catalog).entries, base.name)
37
67
  end
38
68
  end
39
69
 
40
- private
70
+ def composed_outcome(chain, name, virtual)
71
+ attributes = chain.attributes(name, virtual: virtual, editable: @sources.flags[:edit_access].granted?)
72
+ attributes ? FieldOutcome.new(name: name, unconfigured: false, **attributes) : unconfigured_outcome(name, virtual)
73
+ end
41
74
 
42
75
  def active_fields(resource)
43
76
  resource.resource_fields.select(&:is_active)
44
77
  end
45
78
 
79
+ # The engine's variants (FieldVariants): only names the model has.
46
80
  def variants_of(name)
47
- @configuration.field_variants_for(name)
81
+ variants.call(name)
48
82
  end
49
83
 
84
+ # The engine's own targets (FieldPermissionMerger.targets_of): a
85
+ # resource's rows under their own names, then the derived variants of
86
+ # the names no row of the resource has.
50
87
  def merge_granting_fields
51
88
  @sources.grants.each do |ps, resource|
52
- active_fields(resource).each { |field| merge_granting_field(ps, field) }
89
+ Services::FieldPermissionMerger.targets_of(active_fields(resource), variants).each do |name, field, variant_of|
90
+ merge_into(entry_for(name, variant_of), field, ps)
91
+ end
53
92
  end
54
93
  implicit_names.each { |name| @entries[name] ||= implicit_entry }
55
94
  end
56
95
 
57
- def merge_granting_field(permission_set, field)
58
- name = field.name
59
- variants_of(name).each { |variant| merge_into(entry_for(variant, name), field, permission_set) }
60
- merge_into(entry_for(name, nil), field, permission_set)
96
+ # An explicit row for the name (variant_of nil) makes the entry a field
97
+ # of its own, whichever set wrote the derived entry first.
98
+ def entry_for(key, variant_of)
99
+ entry = (@entries[key] ||= { sources: [], variant_of: variant_of })
100
+ entry[:variant_of] = nil if variant_of.nil?
101
+ entry
61
102
  end
62
103
 
63
- def entry_for(key, variant_of)
64
- @entries[key] ||= { sources: [], variant_of: variant_of }
104
+ def variants
105
+ @variants ||= FieldVariants.new(@sources.name, @configuration)
65
106
  end
66
107
 
67
108
  # The engine's own merge (FieldPermissionMerger.merge_row), plus provenance.
68
109
  def merge_into(entry, field, permission_set)
69
110
  Services::FieldPermissionMerger.merge_row(entry, field)
70
- entry[:sources] << [permission_set, FieldLevel.level_of(field, muting: false), field.mask_strategy]
111
+ entry[:sources] << [permission_set, FieldLevel.level_of(field, muting: false), field.mask_strategy, @sources.name]
71
112
  end
72
113
 
73
114
  # Declared names that get the implicit read + edit entry; none under
@@ -75,7 +116,7 @@ module PermissionForge
75
116
  # unconfigured virtual fields instead). Read for any constant, like the
76
117
  # engine does, not only for the models the catalog lists.
77
118
  def implicit_names
78
- PermissionForge::VirtualFields.new(@sources.name.to_s.safe_constantize, @configuration).implicit
119
+ PermissionForge::VirtualFields.new(PermissionForge::ModelLookup.model_for(@sources.name), @configuration).implicit
79
120
  end
80
121
 
81
122
  # The engine's entry for the resource flags as the granting sets merged
@@ -86,86 +127,53 @@ module PermissionForge
86
127
  end
87
128
 
88
129
  def apply_muting_fields
130
+ @entries.each_value { |entry| entry[:before] = entry.slice(:read_access, :edit_access, :mask_access) }
89
131
  @sources.mutes.each do |ps, resource|
90
- active_fields(resource).each { |field| apply_muting_field(ps, field) }
91
- hide_every_field(ps) if resource.read_access
132
+ active_fields(resource).each { |field| apply_muting_field(ps, field, resource.name) }
133
+ hide_every_field(ps, resource.name) if resource.read_access
92
134
  end
93
135
  end
94
136
 
95
137
  # A muting set that revokes the resource's read hides every field, as
96
138
  # the engine does (PermissionHashBuilder#mute_fields_of_resource). One
97
- # that revokes its edit caps every field to read: level_attrs does that
139
+ # that revokes its edit caps every field to read: FieldProvenance does that
98
140
  # from the resource's edit outcome.
99
- def hide_every_field(permission_set)
100
- @entries.each_value { |entry| hide_entry(entry, permission_set) }
141
+ def hide_every_field(permission_set, resource_name)
142
+ @entries.each_value { |entry| hide_entry(entry, permission_set, resource_name) }
101
143
  end
102
144
 
103
- def hide_entry(entry, permission_set)
145
+ def hide_entry(entry, permission_set, resource_name)
104
146
  entry[:read_access] = false
105
147
  entry[:edit_access] = false
106
- (entry[:mutes] ||= []) << [permission_set, 'hide']
148
+ (entry[:mutes] ||= []) << [permission_set, 'hide', resource_name]
107
149
  end
108
150
 
109
- def apply_muting_field(permission_set, field)
151
+ def apply_muting_field(permission_set, field, resource_name)
110
152
  name = field.name
111
153
  ([name] + variants_of(name)).each do |key|
112
154
  entry = @entries[key]
113
- mute_field(entry, field, permission_set) if entry
155
+ mute_field(entry, field, [permission_set, resource_name]) if entry
114
156
  end
115
157
  end
116
158
 
117
159
  # The engine's own muting of a row (PermissionHashBuilder
118
160
  # #mute_single_field: a row that mutes read mutes edit too), plus
119
- # provenance.
120
- def mute_field(entry, field, permission_set)
121
- level = FieldLevel.level_of(field, muting: true)
161
+ # provenance (`muting`: [set, the name of the Resource of the row]).
162
+ def mute_field(entry, field, muting)
163
+ level = FieldLevel.muting_provenance(field)
122
164
  return if level == 'leave'
123
165
 
124
166
  entry[:read_access] = false if field.read_access
125
167
  entry[:edit_access] = false if field.read_access || field.edit_access
126
168
  entry[:mask_access] = true if field.mask_access
127
- (entry[:mutes] ||= []) << [permission_set, level]
128
- end
129
-
130
- def field_outcome(name, virtual)
131
- entry = @entries[name]
132
- return unconfigured_outcome(name, virtual) if entry.nil?
133
-
134
- implicit = entry[:extra].present?
135
- FieldOutcome.new(
136
- name: name, mask_strategy: entry[:mask_strategy], granted_by: granting_sources(entry),
137
- unconfigured: false, virtual: virtual || implicit, implicit: implicit, variant_of: entry[:variant_of],
138
- **level_attrs(entry), **muting_attrs(entry)
139
- )
140
- end
141
-
142
- # Editable fields are capped to Read when the resource's Edit is not granted.
143
- def level_attrs(entry)
144
- level = FieldLevel.effective_level(entry)
145
- capped = level == 'edit' && !@sources.flags[:edit_access].granted?
146
- { level: capped ? 'read' : level, capped: capped }
147
- end
148
-
149
- def muting_attrs(entry)
150
- mutes = entry[:mutes] || []
151
- { blocked_by: mutes.map(&:first).uniq, blocked_level: mutes.last&.last }
152
- end
153
-
154
- # Sets that make the field readable, most open first. A set that lists
155
- # the field as Hidden grants nothing and, like the engine, has no say in
156
- # the mask. An implicit entry exists only when no set configured the
157
- # field, so no set is credited for it: the level comes from the
158
- # virtual-field default.
159
- def granting_sources(entry)
160
- return [] if entry[:extra]
161
-
162
- entry[:sources].select { |_, lvl, _| FieldLevel.rank(lvl) >= 1 }
163
- .sort_by { |_, lvl, _| -FieldLevel.rank(lvl) }.map(&:first).uniq
169
+ permission_set, resource_name = muting
170
+ (entry[:mutes] ||= []) << [permission_set, level, resource_name]
164
171
  end
165
172
 
166
173
  def unconfigured_outcome(name, virtual)
167
- FieldOutcome.new(name: name, level: unconfigured_level, mask_strategy: 'full', granted_by: [],
168
- blocked_by: [], capped: false, unconfigured: true, virtual: virtual, implicit: false)
174
+ FieldOutcome.new(name: name, level: unconfigured_level, mask_strategy: 'full', granted_by: [], grants: [],
175
+ blocked_by: [], mutes: [], capped: false, unconfigured: true, virtual: virtual, implicit: false,
176
+ decided_by: nil, unreadable: false)
169
177
  end
170
178
 
171
179
  def unconfigured_level
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Admin
5
+ # A model's refusals as sentences in the page's language ("Name already
6
+ # exists in this permission set"), from the gem's own keys: Rails'
7
+ # `full_messages` reads `errors.format` and `human_attribute_name`,
8
+ # English in a host without rails-i18n. The attribute is named by
9
+ # `permission_forge.validations.attributes.<attribute>`, else by the
10
+ # attribute's stored name verbatim; the message is the validator's
11
+ # (every validator of the gem's models has a `permission_forge.validations.*` key).
12
+ #
13
+ # @example
14
+ # ErrorSentence.all(record.errors) # => ["Name already exists in this permission set"]
15
+ #
16
+ # @api private
17
+ module ErrorSentence
18
+ SCOPE = 'permission_forge.validations'
19
+
20
+ # @param errors [ActiveModel::Errors]
21
+ # @return [Array<String>]
22
+ def self.all(errors)
23
+ errors.map { |error| sentence(error.attribute, error.message) }
24
+ end
25
+
26
+ # @param errors [ActiveModel::Errors]
27
+ # @return [String, nil]
28
+ def self.first(errors)
29
+ all(errors).first
30
+ end
31
+
32
+ # @param attribute [Symbol, String]
33
+ # @param message [String] the validator's message
34
+ # @return [String]
35
+ def self.sentence(attribute, message)
36
+ return message.to_s if attribute.to_s == 'base'
37
+
38
+ I18n.t("#{SCOPE}.full_message", attribute: attribute_label(attribute), message: message)
39
+ end
40
+
41
+ # @return [String] the attribute's label, else its name as stored
42
+ def self.attribute_label(attribute)
43
+ I18n.t("#{SCOPE}.attributes.#{attribute}", default: attribute.to_s)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -9,6 +9,9 @@ module PermissionForge
9
9
  DAYS = { '1d' => 1, '1w' => 7, '30d' => 30 }.freeze
10
10
  KEYS = %w[1d 1w 30d eom custom never].freeze
11
11
  CHANGE_KEYS = %w[1w 30d eom never].freeze
12
+ # Home's "Extend 30 days": 30 days past the current end date (or today).
13
+ EXTEND_KEY = 'extend30'
14
+ EXTEND_DAYS = 30
12
15
 
13
16
  class InvalidDate < StandardError; end
14
17
 
@@ -28,6 +31,20 @@ module PermissionForge
28
31
  date_for(key, today: today, custom: custom)&.in_time_zone&.end_of_day
29
32
  end
30
33
 
34
+ # The end date a change of an assignment's end date gives: a CHANGE_KEYS
35
+ # preset, or EXTEND_KEY (EXTEND_DAYS past `from`, or past today when
36
+ # `from` is earlier or nil). :invalid for any other key.
37
+ #
38
+ # @param from [Time, nil] the end date the assignment has
39
+ # @return [Time, nil, :invalid]
40
+ def change_to(key, from:, today: Time.zone.today)
41
+ key = key.to_s
42
+ return ([from&.to_date, today].compact.max + EXTEND_DAYS).in_time_zone.end_of_day if key == EXTEND_KEY
43
+ return :invalid unless CHANGE_KEYS.include?(key)
44
+
45
+ expires_at_for(key, today: today)
46
+ end
47
+
31
48
  def parse_custom(value, today)
32
49
  raise InvalidDate, :missing if value.blank?
33
50
 
@@ -7,7 +7,10 @@ module PermissionForge
7
7
  # Permission Set, when and by whom it was granted, when it ends, when
8
8
  # and by whom it was last reviewed, and the reason of its last change.
9
9
  # Granted by is the actor of the assignment.granted event when the
10
- # audit log has one, else the created_by column.
10
+ # audit log has one — named as the admin names it (Admin::ActorLabel:
11
+ # a code such as an API client nobody named in the export's language;
12
+ # the actor's kind and id stay as stored, and the events export keeps
13
+ # the stored label) — else the created_by column.
11
14
  #
12
15
  # The rows are read while the file is written (batches of BATCH_SIZE,
13
16
  # the way the events export does), never all at once. `row_count` in the
@@ -59,7 +62,7 @@ module PermissionForge
59
62
 
60
63
  def each_batch(assignments)
61
64
  assignments.find_in_batches(batch_size: @batch_size) do |batch|
62
- granted = LastChanges.new(batch, events: %w[assignment.granted])
65
+ granted = LastChanges.creations(batch)
63
66
  people = people_by_id(batch)
64
67
  batch.each { |assignment| yield row(assignment, granted[assignment], people) }
65
68
  end
@@ -87,7 +90,7 @@ module PermissionForge
87
90
  def granted_by(assignment, event, people)
88
91
  if event
89
92
  return { 'granted_by_type' => event.actor_type, 'granted_by_id' => event.actor_id,
90
- 'granted_by' => event.actor_label }
93
+ 'granted_by' => ActorLabel.for(event) }
91
94
  end
92
95
 
93
96
  id = assignment.try(:created_by_id)
@@ -2,9 +2,11 @@
2
2
 
3
3
  module PermissionForge
4
4
  module Admin
5
- # Every Permission Set containing a resource, with one field's row there
6
- # (granting sets first, then by name) and the set's people count. Backs
7
- # the field drawer's "In other sets" list and "Who can see …?".
5
+ # Every Permission Set containing a resource — or, for a
6
+ # single-table-inheritance subclass, one of the bases its records answer
7
+ # to (ResourceChain: a base's field row reaches the subclass) — with one
8
+ # field's row there (granting sets first, then by name) and the set's
9
+ # people count. Backs the field drawer's "In other sets" list and "Who can see …?".
8
10
  class FieldAcrossSets
9
11
  # @return [Array<Hash>] { resource:, set:, field: (ResourceField or nil), people:, in_effect: }
10
12
  # in_effect is whether the row (or, without a row, the resource) counts
@@ -27,7 +29,8 @@ module PermissionForge
27
29
  private
28
30
 
29
31
  def resources
30
- PermissionForge::Resource.where(name: @resource_name).includes(:permission_set, :resource_fields)
32
+ names = PermissionForge::ResourceChain.names_for(@resource_name).map(&:to_s)
33
+ PermissionForge::Resource.where(name: names).includes(:resource_fields, permission_set: :resources)
31
34
  .sort_by { |resource| sort_key(resource.permission_set) }
32
35
  end
33
36