permission_forge 0.14.1

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 (406) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +1138 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +356 -0
  5. data/app/controllers/concerns/permission_forge/acting_admin.rb +17 -0
  6. data/app/controllers/concerns/permission_forge/api/crud_actions.rb +84 -0
  7. data/app/controllers/concerns/permission_forge/api/nested_resource_lookup.rb +18 -0
  8. data/app/controllers/concerns/permission_forge/api/pagination.rb +50 -0
  9. data/app/controllers/concerns/permission_forge/api/response_renderer.rb +65 -0
  10. data/app/controllers/concerns/permission_forge/api/serializer_resolver.rb +30 -0
  11. data/app/controllers/concerns/permission_forge/api_authentication.rb +40 -0
  12. data/app/controllers/concerns/permission_forge/assignment_authorization.rb +97 -0
  13. data/app/controllers/concerns/permission_forge/audit_log_gate.rb +55 -0
  14. data/app/controllers/concerns/permission_forge/basic_authentication.rb +77 -0
  15. data/app/controllers/concerns/permission_forge/cache_confirmation.rb +32 -0
  16. data/app/controllers/concerns/permission_forge/forgery_protection.rb +29 -0
  17. data/app/controllers/concerns/permission_forge/html_format.rb +40 -0
  18. data/app/controllers/concerns/permission_forge/param_shapes.rb +87 -0
  19. data/app/controllers/concerns/permission_forge/tenant_required.rb +42 -0
  20. data/app/controllers/permission_forge/api/audit_events_controller.rb +33 -0
  21. data/app/controllers/permission_forge/api/base_controller.rb +220 -0
  22. data/app/controllers/permission_forge/api/dashboard_controller.rb +30 -0
  23. data/app/controllers/permission_forge/api/permission_set_group_users_controller.rb +51 -0
  24. data/app/controllers/permission_forge/api/permission_set_groups_controller.rb +71 -0
  25. data/app/controllers/permission_forge/api/permission_set_rules_controller.rb +37 -0
  26. data/app/controllers/permission_forge/api/permission_set_users_controller.rb +105 -0
  27. data/app/controllers/permission_forge/api/permission_sets_controller.rb +87 -0
  28. data/app/controllers/permission_forge/api/resource_actions_controller.rb +58 -0
  29. data/app/controllers/permission_forge/api/resource_fields_controller.rb +30 -0
  30. data/app/controllers/permission_forge/api/resources_controller.rb +56 -0
  31. data/app/controllers/permission_forge/application_controller.rb +198 -0
  32. data/app/controllers/permission_forge/assets_controller.rb +97 -0
  33. data/app/controllers/permission_forge/assignments_controller.rb +266 -0
  34. data/app/controllers/permission_forge/audit_events_controller.rb +94 -0
  35. data/app/controllers/permission_forge/bulk_assignments_controller.rb +108 -0
  36. data/app/controllers/permission_forge/field_lookups_controller.rb +38 -0
  37. data/app/controllers/permission_forge/home_controller.rb +24 -0
  38. data/app/controllers/permission_forge/maintenance_controller.rb +13 -0
  39. data/app/controllers/permission_forge/people_controller.rb +190 -0
  40. data/app/controllers/permission_forge/permission_set_editor.rb +71 -0
  41. data/app/controllers/permission_forge/permission_set_group_users_controller.rb +57 -0
  42. data/app/controllers/permission_forge/permission_set_groups_controller.rb +111 -0
  43. data/app/controllers/permission_forge/permission_set_rules_controller.rb +97 -0
  44. data/app/controllers/permission_forge/permission_sets_controller.rb +227 -0
  45. data/app/controllers/permission_forge/preferences_controller.rb +26 -0
  46. data/app/controllers/permission_forge/resource_actions_controller.rb +106 -0
  47. data/app/controllers/permission_forge/resource_fields_controller.rb +129 -0
  48. data/app/controllers/permission_forge/resources_controller.rb +189 -0
  49. data/app/controllers/permission_forge/settings_controller.rb +33 -0
  50. data/app/frontend/permission_forge/brand/apple-touch-icon.png +0 -0
  51. data/app/frontend/permission_forge/brand/chevron.svg +1 -0
  52. data/app/frontend/permission_forge/brand/favicon.ico +0 -0
  53. data/app/frontend/permission_forge/brand/favicon.svg +1 -0
  54. data/app/frontend/permission_forge/brand/logo-horizontal-dark.svg +1 -0
  55. data/app/frontend/permission_forge/brand/logo-horizontal-light.svg +1 -0
  56. data/app/frontend/permission_forge/brand/mark-small.svg +1 -0
  57. data/app/frontend/permission_forge/brand/mark.svg +1 -0
  58. data/app/frontend/permission_forge/pf_admin.css +766 -0
  59. data/app/frontend/permission_forge/pf_admin.js +482 -0
  60. data/app/helpers/permission_forge/application_helper.rb +167 -0
  61. data/app/helpers/permission_forge/audit_helper.rb +230 -0
  62. data/app/helpers/permission_forge/audit_sentences_helper.rb +84 -0
  63. data/app/helpers/permission_forge/badges_helper.rb +134 -0
  64. data/app/helpers/permission_forge/changes_helper.rb +117 -0
  65. data/app/helpers/permission_forge/people_helper.rb +118 -0
  66. data/app/helpers/permission_forge/sentences_helper.rb +175 -0
  67. data/app/helpers/permission_forge/settings_helper.rb +152 -0
  68. data/app/helpers/permission_forge/ui_helper.rb +184 -0
  69. data/app/models/permission_forge/audit_event.rb +159 -0
  70. data/app/models/permission_forge/permission_set.rb +100 -0
  71. data/app/models/permission_forge/permission_set_group.rb +83 -0
  72. data/app/models/permission_forge/permission_set_group_user.rb +43 -0
  73. data/app/models/permission_forge/permission_set_rule.rb +99 -0
  74. data/app/models/permission_forge/permission_set_user.rb +158 -0
  75. data/app/models/permission_forge/resource.rb +100 -0
  76. data/app/models/permission_forge/resource_action.rb +58 -0
  77. data/app/models/permission_forge/resource_field.rb +69 -0
  78. data/app/views/layouts/permission_forge/application.html.erb +30 -0
  79. data/app/views/layouts/permission_forge/fragment.html.erb +2 -0
  80. data/app/views/permission_forge/assignments/_person_options.html.erb +26 -0
  81. data/app/views/permission_forge/assignments/confirm_destroy.html.erb +24 -0
  82. data/app/views/permission_forge/assignments/new.html.erb +99 -0
  83. data/app/views/permission_forge/audit_events/index.html.erb +161 -0
  84. data/app/views/permission_forge/audit_events/show.html.erb +146 -0
  85. data/app/views/permission_forge/bulk_assignments/confirm_revoke.html.erb +21 -0
  86. data/app/views/permission_forge/bulk_assignments/new.html.erb +69 -0
  87. data/app/views/permission_forge/field_lookups/show.html.erb +32 -0
  88. data/app/views/permission_forge/home/_audit_card.html.erb +19 -0
  89. data/app/views/permission_forge/home/_ending_card.html.erb +32 -0
  90. data/app/views/permission_forge/home/_expired_card.html.erb +34 -0
  91. data/app/views/permission_forge/home/_privacy_card.html.erb +67 -0
  92. data/app/views/permission_forge/home/_rules_card.html.erb +23 -0
  93. data/app/views/permission_forge/home/_stale_card.html.erb +39 -0
  94. data/app/views/permission_forge/home/_tidy_card.html.erb +18 -0
  95. data/app/views/permission_forge/home/index.html.erb +69 -0
  96. data/app/views/permission_forge/people/_access_tab.html.erb +58 -0
  97. data/app/views/permission_forge/people/_audit_tab.html.erb +93 -0
  98. data/app/views/permission_forge/people/_effective_field_source.html.erb +11 -0
  99. data/app/views/permission_forge/people/_effective_tab.html.erb +118 -0
  100. data/app/views/permission_forge/people/_effective_why.html.erb +48 -0
  101. data/app/views/permission_forge/people/_why_item.html.erb +19 -0
  102. data/app/views/permission_forge/people/confirm_remove_all.html.erb +27 -0
  103. data/app/views/permission_forge/people/consent_preview.html.erb +39 -0
  104. data/app/views/permission_forge/people/erasure_preview.html.erb +28 -0
  105. data/app/views/permission_forge/people/index.html.erb +78 -0
  106. data/app/views/permission_forge/people/show.html.erb +54 -0
  107. data/app/views/permission_forge/permission_set_groups/confirm_destroy.html.erb +12 -0
  108. data/app/views/permission_forge/permission_set_groups/index.html.erb +55 -0
  109. data/app/views/permission_forge/permission_set_groups/show.html.erb +100 -0
  110. data/app/views/permission_forge/permission_set_rules/_builder.html.erb +145 -0
  111. data/app/views/permission_forge/permission_set_rules/_value_input.html.erb +42 -0
  112. data/app/views/permission_forge/permission_set_rules/confirm_destroy.html.erb +13 -0
  113. data/app/views/permission_forge/permission_set_rules/edit.html.erb +3 -0
  114. data/app/views/permission_forge/permission_set_rules/new.html.erb +3 -0
  115. data/app/views/permission_forge/permission_sets/_assignments_tab.html.erb +74 -0
  116. data/app/views/permission_forge/permission_sets/_audit_tab.html.erb +80 -0
  117. data/app/views/permission_forge/permission_sets/_conditions_tab.html.erb +83 -0
  118. data/app/views/permission_forge/permission_sets/_header.html.erb +36 -0
  119. data/app/views/permission_forge/permission_sets/_records_summary.html.erb +16 -0
  120. data/app/views/permission_forge/permission_sets/_resource_summary.html.erb +11 -0
  121. data/app/views/permission_forge/permission_sets/_resources_tab.html.erb +49 -0
  122. data/app/views/permission_forge/permission_sets/confirm_destroy.html.erb +23 -0
  123. data/app/views/permission_forge/permission_sets/confirm_switch_mode.html.erb +15 -0
  124. data/app/views/permission_forge/permission_sets/index.html.erb +83 -0
  125. data/app/views/permission_forge/permission_sets/new.html.erb +63 -0
  126. data/app/views/permission_forge/permission_sets/review.html.erb +26 -0
  127. data/app/views/permission_forge/permission_sets/show.html.erb +50 -0
  128. data/app/views/permission_forge/resource_actions/_across_sets.html.erb +24 -0
  129. data/app/views/permission_forge/resource_actions/_add_select.html.erb +12 -0
  130. data/app/views/permission_forge/resource_actions/_effect.html.erb +25 -0
  131. data/app/views/permission_forge/resource_actions/_endpoints.html.erb +16 -0
  132. data/app/views/permission_forge/resource_actions/_form.html.erb +44 -0
  133. data/app/views/permission_forge/resource_actions/_managed_note.html.erb +11 -0
  134. data/app/views/permission_forge/resource_actions/confirm_destroy.html.erb +13 -0
  135. data/app/views/permission_forge/resource_actions/new.html.erb +31 -0
  136. data/app/views/permission_forge/resource_actions/show.html.erb +39 -0
  137. data/app/views/permission_forge/resource_fields/_explain.html.erb +24 -0
  138. data/app/views/permission_forge/resource_fields/show.html.erb +186 -0
  139. data/app/views/permission_forge/resources/_actions_tab.html.erb +88 -0
  140. data/app/views/permission_forge/resources/_fields_tab.html.erb +89 -0
  141. data/app/views/permission_forge/resources/_rules_tab.html.erb +55 -0
  142. data/app/views/permission_forge/resources/_sync.html.erb +57 -0
  143. data/app/views/permission_forge/resources/confirm_destroy.html.erb +17 -0
  144. data/app/views/permission_forge/resources/new.html.erb +57 -0
  145. data/app/views/permission_forge/resources/show.html.erb +117 -0
  146. data/app/views/permission_forge/settings/_ladder.html.erb +15 -0
  147. data/app/views/permission_forge/settings/_row.html.erb +38 -0
  148. data/app/views/permission_forge/settings/show.html.erb +67 -0
  149. data/app/views/permission_forge/shared/_across_sets.html.erb +31 -0
  150. data/app/views/permission_forge/shared/_audit_events.html.erb +36 -0
  151. data/app/views/permission_forge/shared/_audit_footer.html.erb +16 -0
  152. data/app/views/permission_forge/shared/_audit_off_note.html.erb +15 -0
  153. data/app/views/permission_forge/shared/_audit_pagination.html.erb +19 -0
  154. data/app/views/permission_forge/shared/_audit_refused_note.html.erb +7 -0
  155. data/app/views/permission_forge/shared/_audit_timeline.html.erb +29 -0
  156. data/app/views/permission_forge/shared/_crud_cell.html.erb +18 -0
  157. data/app/views/permission_forge/shared/_dialog_frame.html.erb +11 -0
  158. data/app/views/permission_forge/shared/_expiry_picker.html.erb +27 -0
  159. data/app/views/permission_forge/shared/_head_extra.html.erb +3 -0
  160. data/app/views/permission_forge/shared/_help_banner.html.erb +10 -0
  161. data/app/views/permission_forge/shared/_level_control.html.erb +18 -0
  162. data/app/views/permission_forge/shared/_mode_banners.html.erb +22 -0
  163. data/app/views/permission_forge/shared/_nav.html.erb +12 -0
  164. data/app/views/permission_forge/shared/_preferences.html.erb +24 -0
  165. data/app/views/permission_forge/shared/_reason_field.html.erb +20 -0
  166. data/app/views/permission_forge/shared/_savebar.html.erb +13 -0
  167. data/app/views/permission_forge/shared/_sheet_frame.html.erb +22 -0
  168. data/app/views/permission_forge/shared/_sidebar.html.erb +18 -0
  169. data/app/views/permission_forge/shared/_tech_details.html.erb +20 -0
  170. data/app/views/permission_forge/shared/_toasts.html.erb +27 -0
  171. data/app/views/permission_forge/shared/_topbar.html.erb +15 -0
  172. data/app/views/permission_forge/shared/_user_select.html.erb +16 -0
  173. data/app/views/permission_forge/shared/audit_refused.html.erb +7 -0
  174. data/config/locales/permission_forge.en.yml +2115 -0
  175. data/config/locales/permission_forge.mk.yml +2166 -0
  176. data/config/locales/permission_forge.sq.yml +2164 -0
  177. data/config/routes.rb +143 -0
  178. data/lib/generators/permission_forge/install/install_generator.rb +105 -0
  179. data/lib/generators/permission_forge/install/templates/initializer.rb.tt +148 -0
  180. data/lib/generators/permission_forge/install/templates/migration.rb.tt +198 -0
  181. data/lib/generators/permission_forge/rls/rls_generator.rb +57 -0
  182. data/lib/generators/permission_forge/rls/templates/enable_permission_forge_rls.rb.tt +15 -0
  183. data/lib/generators/permission_forge/upgrade/templates/add_admin_ui_columns.rb.tt +8 -0
  184. data/lib/generators/permission_forge/upgrade/templates/add_audit_events.rb.tt +98 -0
  185. data/lib/generators/permission_forge/upgrade/templates/add_resource_fields_unique_index.rb.tt +69 -0
  186. data/lib/generators/permission_forge/upgrade/upgrade_generator.rb +66 -0
  187. data/lib/permission_forge/admin/action_across_sets.rb +76 -0
  188. data/lib/permission_forge/admin/action_rows.rb +156 -0
  189. data/lib/permission_forge/admin/attention_items.rb +207 -0
  190. data/lib/permission_forge/admin/audit_event_detail.rb +75 -0
  191. data/lib/permission_forge/admin/audit_log.rb +193 -0
  192. data/lib/permission_forge/admin/audit_log_days.rb +41 -0
  193. data/lib/permission_forge/admin/audit_lookups.rb +105 -0
  194. data/lib/permission_forge/admin/audit_name_history.rb +85 -0
  195. data/lib/permission_forge/admin/audit_trail.rb +169 -0
  196. data/lib/permission_forge/admin/bulk_people_matcher.rb +109 -0
  197. data/lib/permission_forge/admin/change_set.rb +123 -0
  198. data/lib/permission_forge/admin/change_set_parts/actions.rb +27 -0
  199. data/lib/permission_forge/admin/change_set_parts/base.rb +58 -0
  200. data/lib/permission_forge/admin/change_set_parts/context.rb +50 -0
  201. data/lib/permission_forge/admin/change_set_parts/field_edit.rb +67 -0
  202. data/lib/permission_forge/admin/change_set_parts/field_levels.rb +71 -0
  203. data/lib/permission_forge/admin/change_set_parts/new_actions.rb +91 -0
  204. data/lib/permission_forge/admin/change_set_parts/new_fields.rb +63 -0
  205. data/lib/permission_forge/admin/change_set_parts/resource_edit.rb +55 -0
  206. data/lib/permission_forge/admin/change_set_parts/resource_flags.rb +19 -0
  207. data/lib/permission_forge/admin/change_set_parts/set_attrs.rb +45 -0
  208. data/lib/permission_forge/admin/change_set_parts/values.rb +20 -0
  209. data/lib/permission_forge/admin/column_fields.rb +48 -0
  210. data/lib/permission_forge/admin/conditions_form.rb +78 -0
  211. data/lib/permission_forge/admin/controller_catalog.rb +166 -0
  212. data/lib/permission_forge/admin/csv.rb +41 -0
  213. data/lib/permission_forge/admin/editable_lock.rb +157 -0
  214. data/lib/permission_forge/admin/effective_access.rb +158 -0
  215. data/lib/permission_forge/admin/effective_fields.rb +176 -0
  216. data/lib/permission_forge/admin/expiry_presets.rb +51 -0
  217. data/lib/permission_forge/admin/exports/document.rb +89 -0
  218. data/lib/permission_forge/admin/exports/events.rb +51 -0
  219. data/lib/permission_forge/admin/exports/holdings.rb +118 -0
  220. data/lib/permission_forge/admin/exports.rb +37 -0
  221. data/lib/permission_forge/admin/field_across_sets.rb +51 -0
  222. data/lib/permission_forge/admin/field_detail.rb +112 -0
  223. data/lib/permission_forge/admin/field_level.rb +74 -0
  224. data/lib/permission_forge/admin/field_rows.rb +137 -0
  225. data/lib/permission_forge/admin/last_changes.rb +94 -0
  226. data/lib/permission_forge/admin/model_catalog.rb +216 -0
  227. data/lib/permission_forge/admin/people_candidates.rb +108 -0
  228. data/lib/permission_forge/admin/people_directory.rb +158 -0
  229. data/lib/permission_forge/admin/permission_set_factory.rb +48 -0
  230. data/lib/permission_forge/admin/person_label.rb +95 -0
  231. data/lib/permission_forge/admin/record_id.rb +19 -0
  232. data/lib/permission_forge/admin/rule_builder.rb +58 -0
  233. data/lib/permission_forge/admin/rule_form.rb +175 -0
  234. data/lib/permission_forge/admin/rule_form_input.rb +74 -0
  235. data/lib/permission_forge/admin/sensitive_columns.rb +68 -0
  236. data/lib/permission_forge/admin/settings_catalog.rb +261 -0
  237. data/lib/permission_forge/admin/settings_values.rb +130 -0
  238. data/lib/permission_forge/api/serializers/audit_event_serializer.rb +27 -0
  239. data/lib/permission_forge/api/serializers/base_serializer.rb +36 -0
  240. data/lib/permission_forge/api/serializers/permission_set_group_serializer.rb +12 -0
  241. data/lib/permission_forge/api/serializers/permission_set_group_user_serializer.rb +11 -0
  242. data/lib/permission_forge/api/serializers/permission_set_rule_serializer.rb +25 -0
  243. data/lib/permission_forge/api/serializers/permission_set_serializer.rb +13 -0
  244. data/lib/permission_forge/api/serializers/permission_set_user_serializer.rb +11 -0
  245. data/lib/permission_forge/api/serializers/resource_action_serializer.rb +13 -0
  246. data/lib/permission_forge/api/serializers/resource_field_serializer.rb +14 -0
  247. data/lib/permission_forge/api/serializers/resource_serializer.rb +15 -0
  248. data/lib/permission_forge/audit/actor.rb +144 -0
  249. data/lib/permission_forge/audit/actor_resolver.rb +103 -0
  250. data/lib/permission_forge/audit/audited_views.rb +208 -0
  251. data/lib/permission_forge/audit/boot_warnings.rb +58 -0
  252. data/lib/permission_forge/audit/column_values.rb +56 -0
  253. data/lib/permission_forge/audit/context.rb +98 -0
  254. data/lib/permission_forge/audit/detached.rb +62 -0
  255. data/lib/permission_forge/audit/event_catalog.rb +110 -0
  256. data/lib/permission_forge/audit/event_query.rb +192 -0
  257. data/lib/permission_forge/audit/failure.rb +47 -0
  258. data/lib/permission_forge/audit/field_access.rb +186 -0
  259. data/lib/permission_forge/audit/gate.rb +167 -0
  260. data/lib/permission_forge/audit/job_context.rb +32 -0
  261. data/lib/permission_forge/audit/log_access.rb +36 -0
  262. data/lib/permission_forge/audit/recorder.rb +168 -0
  263. data/lib/permission_forge/audit/refusals.rb +61 -0
  264. data/lib/permission_forge/audit/request_context.rb +152 -0
  265. data/lib/permission_forge/audit/runtime.rb +178 -0
  266. data/lib/permission_forge/audit/runtime_emission.rb +135 -0
  267. data/lib/permission_forge/audit/settings_observer.rb +70 -0
  268. data/lib/permission_forge/audit/source.rb +54 -0
  269. data/lib/permission_forge/audit/subscribers/base.rb +69 -0
  270. data/lib/permission_forge/audit/subscribers/legacy_hooks.rb +97 -0
  271. data/lib/permission_forge/audit/subscribers/log.rb +45 -0
  272. data/lib/permission_forge/audit/subscribers.rb +58 -0
  273. data/lib/permission_forge/audit.rb +302 -0
  274. data/lib/permission_forge/boot_checks.rb +184 -0
  275. data/lib/permission_forge/cache/backlog.rb +142 -0
  276. data/lib/permission_forge/cache/base.rb +200 -0
  277. data/lib/permission_forge/cache/bulk_invalidation.rb +131 -0
  278. data/lib/permission_forge/cache/failure.rb +171 -0
  279. data/lib/permission_forge/cache/handled_errors.rb +82 -0
  280. data/lib/permission_forge/cache/holder_invalidation.rb +85 -0
  281. data/lib/permission_forge/cache/invalidation.rb +180 -0
  282. data/lib/permission_forge/cache/memory_cache.rb +77 -0
  283. data/lib/permission_forge/cache/null_cache.rb +40 -0
  284. data/lib/permission_forge/cache/rails_cache.rb +231 -0
  285. data/lib/permission_forge/cache/rebuild.rb +149 -0
  286. data/lib/permission_forge/cache/settlement.rb +63 -0
  287. data/lib/permission_forge/cache/user_keys.rb +206 -0
  288. data/lib/permission_forge/cache/variant_removal.rb +55 -0
  289. data/lib/permission_forge/cache/withheld_notice.rb +60 -0
  290. data/lib/permission_forge/concerns/audited_collection.rb +34 -0
  291. data/lib/permission_forge/concerns/auto_permission_scoping.rb +158 -0
  292. data/lib/permission_forge/concerns/controller_action_guard.rb +106 -0
  293. data/lib/permission_forge/concerns/field_mask_checker.rb +45 -0
  294. data/lib/permission_forge/concerns/has_active_scope.rb +12 -0
  295. data/lib/permission_forge/concerns/has_audit_trail.rb +59 -0
  296. data/lib/permission_forge/concerns/has_permission_set_users.rb +115 -0
  297. data/lib/permission_forge/concerns/has_tenant_scoping.rb +86 -0
  298. data/lib/permission_forge/concerns/has_uuid.rb +24 -0
  299. data/lib/permission_forge/concerns/invalidates_in_batch.rb +207 -0
  300. data/lib/permission_forge/concerns/invalidates_managed_actions.rb +80 -0
  301. data/lib/permission_forge/concerns/masked_attributes/alias_access.rb +280 -0
  302. data/lib/permission_forge/concerns/masked_attributes/store_access.rb +100 -0
  303. data/lib/permission_forge/concerns/masked_attributes/validation_reads.rb +153 -0
  304. data/lib/permission_forge/concerns/masked_attributes/virtual_readers.rb +242 -0
  305. data/lib/permission_forge/concerns/masked_attributes.rb +576 -0
  306. data/lib/permission_forge/concerns/permission_cache_accessor.rb +416 -0
  307. data/lib/permission_forge/concerns/permission_scoped_findable.rb +337 -0
  308. data/lib/permission_forge/concerns/permissioned_user.rb +78 -0
  309. data/lib/permission_forge/concerns/permissions_memo.rb +160 -0
  310. data/lib/permission_forge/concerns/permissions_per_context.rb +288 -0
  311. data/lib/permission_forge/concerns/permissions_regenerator.rb +43 -0
  312. data/lib/permission_forge/concerns/records_audit_events.rb +286 -0
  313. data/lib/permission_forge/concerns/resource_permission_checker.rb +116 -0
  314. data/lib/permission_forge/concerns/validates_column_lengths.rb +69 -0
  315. data/lib/permission_forge/concerns/validates_dependency.rb +54 -0
  316. data/lib/permission_forge/conditional_sets.rb +361 -0
  317. data/lib/permission_forge/configuration/admin_ui_settings.rb +108 -0
  318. data/lib/permission_forge/configuration/audit_settings.rb +184 -0
  319. data/lib/permission_forge/configuration/cache_settings.rb +76 -0
  320. data/lib/permission_forge/configuration/controller_action_settings.rb +112 -0
  321. data/lib/permission_forge/configuration/gdpr_settings.rb +70 -0
  322. data/lib/permission_forge/configuration/masked_attributes_settings.rb +83 -0
  323. data/lib/permission_forge/configuration/permits_attributes_settings.rb +26 -0
  324. data/lib/permission_forge/configuration/presets.rb +139 -0
  325. data/lib/permission_forge/configuration/query_scoping_settings.rb +107 -0
  326. data/lib/permission_forge/configuration/strategy_values.rb +57 -0
  327. data/lib/permission_forge/configuration/virtual_attributes_settings.rb +65 -0
  328. data/lib/permission_forge/configuration.rb +624 -0
  329. data/lib/permission_forge/controller_actions/authorizer.rb +171 -0
  330. data/lib/permission_forge/controller_actions/managed_index.rb +213 -0
  331. data/lib/permission_forge/controller_actions/mapping.rb +167 -0
  332. data/lib/permission_forge/controller_actions.rb +122 -0
  333. data/lib/permission_forge/deprecation.rb +86 -0
  334. data/lib/permission_forge/engine.rb +113 -0
  335. data/lib/permission_forge/execution_state.rb +318 -0
  336. data/lib/permission_forge/invalidation_batch.rb +206 -0
  337. data/lib/permission_forge/masked_models.rb +226 -0
  338. data/lib/permission_forge/masking_sync.rb +271 -0
  339. data/lib/permission_forge/model_bindings.rb +90 -0
  340. data/lib/permission_forge/open_transaction.rb +303 -0
  341. data/lib/permission_forge/operators/constants.rb +41 -0
  342. data/lib/permission_forge/operators/registry.rb +300 -0
  343. data/lib/permission_forge/operators/rule_column.rb +120 -0
  344. data/lib/permission_forge/operators/validations.rb +111 -0
  345. data/lib/permission_forge/operators/value_casters/number.rb +68 -0
  346. data/lib/permission_forge/operators/value_casters/temporal.rb +69 -0
  347. data/lib/permission_forge/operators/value_casters.rb +80 -0
  348. data/lib/permission_forge/operators/value_typing.rb +115 -0
  349. data/lib/permission_forge/query_scoping_warnings.rb +68 -0
  350. data/lib/permission_forge/rake_audit.rb +65 -0
  351. data/lib/permission_forge/resource_chain.rb +190 -0
  352. data/lib/permission_forge/rls.rb +209 -0
  353. data/lib/permission_forge/services/attribute_access_guard.rb +138 -0
  354. data/lib/permission_forge/services/audit_event_purger.rb +150 -0
  355. data/lib/permission_forge/services/bulk_permission_set_assignment.rb +108 -0
  356. data/lib/permission_forge/services/concerns/across_tenants.rb +69 -0
  357. data/lib/permission_forge/services/concerns/audited_operation.rb +43 -0
  358. data/lib/permission_forge/services/concerns/clonable.rb +29 -0
  359. data/lib/permission_forge/services/consent_withdrawal_handler.rb +135 -0
  360. data/lib/permission_forge/services/dashboard_stats.rb +43 -0
  361. data/lib/permission_forge/services/data_subject_erasure.rb +306 -0
  362. data/lib/permission_forge/services/expired_assignment_cleaner.rb +152 -0
  363. data/lib/permission_forge/services/extra_field_defaults.rb +71 -0
  364. data/lib/permission_forge/services/field_permission_merger.rb +83 -0
  365. data/lib/permission_forge/services/filters_response_attributes.rb +144 -0
  366. data/lib/permission_forge/services/gdpr_field_auditor/text_report.rb +63 -0
  367. data/lib/permission_forge/services/gdpr_field_auditor.rb +223 -0
  368. data/lib/permission_forge/services/host_key_guard.rb +110 -0
  369. data/lib/permission_forge/services/mask_value.rb +93 -0
  370. data/lib/permission_forge/services/permission_cache_clearer.rb +174 -0
  371. data/lib/permission_forge/services/permission_hash_builder.rb +305 -0
  372. data/lib/permission_forge/services/permission_rule_applier.rb +192 -0
  373. data/lib/permission_forge/services/permission_set_cloner.rb +89 -0
  374. data/lib/permission_forge/services/permission_set_condition_evaluator.rb +102 -0
  375. data/lib/permission_forge/services/permission_set_group_cloner.rb +52 -0
  376. data/lib/permission_forge/services/permits_attributes/nested_records.rb +106 -0
  377. data/lib/permission_forge/services/permits_attributes.rb +270 -0
  378. data/lib/permission_forge/services/resource_field_generator.rb +93 -0
  379. data/lib/permission_forge/services/resource_with_fields_builder.rb +47 -0
  380. data/lib/permission_forge/services/rls_operator_sql.rb +131 -0
  381. data/lib/permission_forge/services/rls_policy_generator.rb +365 -0
  382. data/lib/permission_forge/services/security_audit/action_check.rb +157 -0
  383. data/lib/permission_forge/services/security_audit/assignment_check.rb +120 -0
  384. data/lib/permission_forge/services/security_audit/audit_trail_check.rb +166 -0
  385. data/lib/permission_forge/services/security_audit/base_check.rb +57 -0
  386. data/lib/permission_forge/services/security_audit/config_check.rb +212 -0
  387. data/lib/permission_forge/services/security_audit/integrity_check.rb +105 -0
  388. data/lib/permission_forge/services/security_audit/personal_data_check.rb +79 -0
  389. data/lib/permission_forge/services/security_audit/privilege_check.rb +67 -0
  390. data/lib/permission_forge/services/security_audit/rule_check.rb +141 -0
  391. data/lib/permission_forge/services/security_audit/text_report.rb +46 -0
  392. data/lib/permission_forge/services/security_audit/user_model_check.rb +67 -0
  393. data/lib/permission_forge/services/security_audit.rb +75 -0
  394. data/lib/permission_forge/services/stale_assignment_auditor.rb +68 -0
  395. data/lib/permission_forge/services/unscoped_query_detector.rb +132 -0
  396. data/lib/permission_forge/version.rb +3 -0
  397. data/lib/permission_forge/virtual_fields/discovery.rb +65 -0
  398. data/lib/permission_forge/virtual_fields.rb +102 -0
  399. data/lib/permission_forge.rb +464 -0
  400. data/lib/tasks/permission_forge.rake +31 -0
  401. data/lib/tasks/permission_forge_audit_events.rake +28 -0
  402. data/lib/tasks/permission_forge_cache.rake +26 -0
  403. data/lib/tasks/permission_forge_expiration.rake +30 -0
  404. data/lib/tasks/permission_forge_gdpr.rake +69 -0
  405. data/lib/tasks/permission_forge_rls.rake +38 -0
  406. metadata +515 -0
@@ -0,0 +1,152 @@
1
+ module PermissionForge
2
+ module Services
3
+ # Removes expired PermissionSetUser records and invalidates the cached
4
+ # permissions of all affected users (rebuilt read-through on next access).
5
+ #
6
+ # @example
7
+ # result = ExpiredAssignmentCleaner.call
8
+ # # => { removed: 5, invalidated: 3 }
9
+ #
10
+ # `invalidated` counts the users whose invalidation the cache store
11
+ # confirmed; a failure is reported through `config.on_cache_failure`.
12
+ #
13
+ # @example Dry run (inspect without removing)
14
+ # result = ExpiredAssignmentCleaner.call(dry_run: true)
15
+ # # => { removed: 0, invalidated: 0, expired_count: 5, affected_user_ids: [1, 2, 3] }
16
+ #
17
+ # Each assignment is re-read, locked, when it is destroyed: one that was
18
+ # renewed (or removed) since the run selected it is left alone, and its
19
+ # `assignment.revoked` snapshot is the row as it was at that moment.
20
+ #
21
+ # The audit log: assignments are destroyed through their callbacks (one
22
+ # `assignment.revoked` each), then ONE `cleanup.expired_assignments` event
23
+ # names the sets and the counts — only when something was removed, so a
24
+ # nightly run over nothing leaves no noise. Its data holds the number of
25
+ # affected users and at most AFFECTED_USER_IDS_LIMIT of their ids
26
+ # (`affected_user_ids_truncated` says when there were more; each
27
+ # `assignment.revoked` names its own user). `actor:` (a user, an
28
+ # Audit::Actor or a label String) and `reason:` say who ran it and why;
29
+ # without a reason (none given, none in the context) the events carry
30
+ # DEFAULT_REASON. A dry run writes nothing.
31
+ #
32
+ # Tenants: with a tenant ambient, that tenant's assignments. With none
33
+ # (the rake task) every tenant's — also under acts_as_tenant's
34
+ # require_tenant — and the summary is written in each tenant that lost
35
+ # assignments (its counts) plus once without a tenant (the totals), each
36
+ # with `across_tenants: true` (Concerns::AcrossTenants).
37
+ #
38
+ # @api public
39
+ class ExpiredAssignmentCleaner
40
+ include Concerns::AuditedOperation
41
+ include Concerns::AcrossTenants
42
+
43
+ DEFAULT_REASON = 'expired'.freeze
44
+
45
+ # The most user ids the summary event stores: the event's data must
46
+ # stay well inside a MySQL TEXT column (64 KB) whatever the run's size.
47
+ AFFECTED_USER_IDS_LIMIT = 100
48
+
49
+ # Removes expired assignments and invalidates the affected users' cached permissions.
50
+ # @return [Hash] see {#call}
51
+ def self.call(...) = new(...).call
52
+
53
+ def initialize(dry_run: false, actor: nil, reason: nil)
54
+ @dry_run = dry_run
55
+ @actor = actor
56
+ @reason = reason
57
+ @removed_count = 0
58
+ @invalidated_count = 0
59
+ end
60
+
61
+ # @return [Hash] `{ removed:, invalidated: }` (plus `expired_count:` and `affected_user_ids:` for a dry run)
62
+ def call
63
+ across_tenants do
64
+ expired_records = PermissionForge::PermissionSetUser.expired
65
+ next dry_run_result(expired_records.count, expired_records.distinct.pluck(:user_id)) if @dry_run
66
+
67
+ audited(actor: @actor, reason: reason) { clean(expired_records.order(:id).pluck(:id)) }
68
+ end
69
+ end
70
+
71
+ private
72
+
73
+ def reason
74
+ return @reason if @reason.present? || PermissionForge::Audit::Context[:reason].present?
75
+
76
+ DEFAULT_REASON
77
+ end
78
+
79
+ def clean(candidate_ids)
80
+ removed = candidate_ids.filter_map { |id| destroy_if_still_expired(id) }
81
+ @removed_count = removed.size
82
+
83
+ confirmed = invalidate_users(removed.map(&:user_id).uniq)
84
+ @invalidated_count = confirmed.size
85
+ record_summary(removed, confirmed) if @removed_count.positive?
86
+
87
+ { removed: @removed_count, invalidated: @invalidated_count }
88
+ end
89
+
90
+ # The row as it is now, locked until it is gone; nil when it is no
91
+ # longer expired (renewed) or no longer there.
92
+ def destroy_if_still_expired(id)
93
+ PermissionForge::PermissionSetUser.transaction do
94
+ record = PermissionForge::PermissionSetUser.expired.lock.find_by(id: id)
95
+ record&.destroy
96
+ end
97
+ end
98
+
99
+ # One event, or one per tenant plus the totals (record_per_tenant),
100
+ # each with the counts of its own rows.
101
+ def record_summary(records, confirmed_user_ids)
102
+ sets = PermissionForge::PermissionSet.where(id: records.map(&:permission_set_id).uniq).index_by(&:id)
103
+ record_per_tenant('cleanup.expired_assignments', records) do |rows|
104
+ SummaryData.new(rows, sets, confirmed_user_ids).to_h
105
+ end
106
+ end
107
+
108
+ # The data of one cleanup.expired_assignments event, for the removed
109
+ # rows it covers.
110
+ #
111
+ # @api private
112
+ SummaryData = Struct.new(:rows, :sets_by_id, :confirmed_user_ids) do
113
+ def to_h
114
+ { 'operation' => 'cleanup_expired_assignments', 'removed' => rows.size,
115
+ 'invalidated' => (user_ids & confirmed_user_ids).size, **bounded_user_ids,
116
+ 'set_uuids' => sets.map(&:uuid).uniq, 'set_names' => sets.map(&:name).uniq, 'dry_run' => false }
117
+ end
118
+
119
+ private
120
+
121
+ def user_ids
122
+ @user_ids ||= rows.map(&:user_id).uniq
123
+ end
124
+
125
+ def sets
126
+ sets_by_id.values_at(*rows.map(&:permission_set_id).uniq.sort).compact
127
+ end
128
+
129
+ def bounded_user_ids
130
+ { 'affected_user_count' => user_ids.size,
131
+ 'affected_user_ids' => user_ids.first(AFFECTED_USER_IDS_LIMIT).map(&:to_s),
132
+ 'affected_user_ids_truncated' => user_ids.size > AFFECTED_USER_IDS_LIMIT }
133
+ end
134
+ end
135
+ private_constant :SummaryData
136
+
137
+ def dry_run_result(expired_count, affected_user_ids)
138
+ { removed: 0, invalidated: 0, expired_count: expired_count, affected_user_ids: affected_user_ids }
139
+ end
140
+
141
+ # @return [Array] the ids of the users whose invalidation the store
142
+ # confirmed — not what a host's override of the method returns
143
+ def invalidate_users(user_ids)
144
+ confirmed = []
145
+ PermissionForge.configuration.user_model.where(id: user_ids).find_each do |user|
146
+ confirmed << user.id if PermissionForge::Cache::Failure.none? { user.invalidate_permissions }
147
+ end
148
+ confirmed
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,71 @@
1
+ module PermissionForge
2
+ module Services
3
+ # Gives a model's declared extra (virtual) fields their implicit entry when
4
+ # nothing configured them: readable when the resource grants read,
5
+ # editable when it grants edit, never masked.
6
+ #
7
+ # Strictly ADDITIVE: a name that already has an entry (a ResourceField row,
8
+ # or a variant of one) is left untouched, so an extra field can never
9
+ # unmask or grant edit on a field an admin configured. PermissionHashBuilder
10
+ # calls this once per resource, after every granting set is merged, so a
11
+ # row in ANY of the user's sets wins whatever order the sets are in, and
12
+ # the resource flags it follows are the merged ones (edit from any set).
13
+ #
14
+ # Only names the model declares through extra_fields_method are read, and
15
+ # only under extra_fields_default_access = :grant. Under :unconfigured
16
+ # nothing is added: the field follows unconfigured_field_strategy, like a
17
+ # column. Names the admin merely discovers never get an entry here.
18
+ #
19
+ # An error raised by the host's extra_fields_method is not rescued: a
20
+ # broken method must fail the build of the hash, not quietly change it.
21
+ #
22
+ # @api private
23
+ class ExtraFieldDefaults
24
+ # The entry's mask part: an implicit entry is never masked.
25
+ UNMASKED = { mask_access: false, mask_strategy: 'full' }.freeze
26
+
27
+ # @param entry [Hash] the resource's entry in the hash being built:
28
+ # its :fields (written in place) and its merged :permissions
29
+ # @return [Hash] the fields, with implicit entries added
30
+ def self.call(entry:, resource_name:, configuration: PermissionForge.configuration)
31
+ new(entry:, resource_name:, configuration:).call
32
+ end
33
+
34
+ # The implicit entry for a resource with the given merged permissions.
35
+ # Only an exact `true` grants: the entry used to be read + edit for
36
+ # everyone who could read the resource, so a read-only user could write
37
+ # a declared virtual field (before 0.14).
38
+ #
39
+ # @param permissions [Hash] the resource's merged :permissions
40
+ # @return [Hash] a new entry
41
+ def self.implicit_entry(permissions)
42
+ { read_access: permissions[:read_access] == true, edit_access: permissions[:edit_access] == true, **UNMASKED }
43
+ end
44
+
45
+ def initialize(entry:, resource_name:, configuration:)
46
+ @fields = entry.fetch(:fields)
47
+ @permissions = entry.fetch(:permissions)
48
+ @resource_name = resource_name.to_s
49
+ @configuration = configuration
50
+ end
51
+
52
+ # @return [Hash] the fields hash, with implicit entries added
53
+ def call
54
+ declared_names.each { |name| @fields[name] ||= self.class.implicit_entry(@permissions) }
55
+ @fields
56
+ end
57
+
58
+ private
59
+
60
+ # @fields is String-keyed until the cache symbolizes it. A Symbol name
61
+ # would miss the collision check, sit beside the configured String key,
62
+ # and replace it once both become Symbols — so VirtualFields.extra_names
63
+ # returns Strings.
64
+ def declared_names
65
+ return [] unless @configuration.grant_extra_fields_by_default?
66
+
67
+ PermissionForge::VirtualFields.extra_names(@resource_name.safe_constantize, @configuration)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,83 @@
1
+ module PermissionForge
2
+ module Services
3
+ # Merges the active ResourceField rows of one resource into a fields hash.
4
+ # Extracted from PermissionHashBuilder for SRP, which calls it once per
5
+ # granting set.
6
+ #
7
+ # Rows only: the implicit entries for a model's extra (virtual) fields are
8
+ # added by ExtraFieldDefaults, once per resource, after every set is merged.
9
+ #
10
+ # {.merge_row} is the one definition of merging a row into an entry; the
11
+ # admin's Admin::EffectiveFields explains fields with it.
12
+ #
13
+ # @api private
14
+ class FieldPermissionMerger
15
+ FIELD_PERMISSION_OPTIONS = %i[read_access edit_access].freeze
16
+ FIELD_MASK_OPTIONS = %i[mask_access mask_strategy].freeze
17
+ # The mask of an entry no row that grants read has decided yet.
18
+ DEFAULT_MASK_STRATEGY = 'full'.freeze
19
+
20
+ def self.call(...) = new(...).call
21
+
22
+ # Merges one row into one entry, across granting sets:
23
+ # - read and edit are OR-ed (any set that grants wins);
24
+ # - the mask is decided by the rows that GRANT READ alone: AND-ed (any
25
+ # such set that unmasks wins), with the first such row's strategy.
26
+ # A row that grants no read (the admin UI's Hidden: read=f, mask=f)
27
+ # never votes — it used to take part in the AND, so Masked in one set
28
+ # plus Hidden in another read as plaintext, in either order.
29
+ # The four keys are always present (the hash format is a contract).
30
+ #
31
+ # @param entry [Hash] the field's entry so far, written in place
32
+ # @param field [ResourceField, #read_access] the row
33
+ # @return [Hash] the entry
34
+ def self.merge_row(entry, field)
35
+ # A row that granted read has merged the mask already: read_access is
36
+ # OR-ed, so it is true from that row on and never before.
37
+ decided = entry[:read_access] == true
38
+ FIELD_PERMISSION_OPTIONS.each { |action| entry[action] ||= field.public_send(action) }
39
+ merge_mask(entry, field, decided)
40
+ entry
41
+ end
42
+
43
+ def self.merge_mask(entry, field, decided)
44
+ unless field.read_access
45
+ entry[:mask_access] = false unless entry.key?(:mask_access)
46
+ entry[:mask_strategy] ||= DEFAULT_MASK_STRATEGY
47
+ return
48
+ end
49
+
50
+ entry[:mask_access] = decided ? entry[:mask_access] && field.mask_access : field.mask_access
51
+ entry[:mask_strategy] = field.mask_strategy unless decided
52
+ end
53
+ private_class_method :merge_mask
54
+
55
+ def initialize(fields:, resource:, configuration: PermissionForge.configuration)
56
+ @fields = fields
57
+ @resource = resource
58
+ @configuration = configuration
59
+ end
60
+
61
+ def call
62
+ @resource.resource_fields.select(&:is_active).each do |field|
63
+ next unless field.is_active
64
+
65
+ compute_field_variants(field.name).each { |variant| set_field_permissions(variant, field) }
66
+ set_field_permissions(field.name, field)
67
+ end
68
+
69
+ @fields
70
+ end
71
+
72
+ private
73
+
74
+ def compute_field_variants(field_name)
75
+ @configuration.field_variants_for(field_name)
76
+ end
77
+
78
+ def set_field_permissions(field_name, field)
79
+ self.class.merge_row(@fields[field_name] ||= {}, field)
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,144 @@
1
+ module PermissionForge
2
+ module Services
3
+ # Filters and masks response attributes based on the current user's field permissions.
4
+ # Fields without read access are removed; fields with mask_access are masked using MaskValue.
5
+ #
6
+ # Fail-closed posture: when a user is present but has NO field permissions for
7
+ # the resource (unmapped resource, cache re-entrancy), every attribute is treated
8
+ # as unconfigured and routed through `unconfigured_field_strategy` (default :block),
9
+ # so an authenticated user never receives raw fields for a resource they lack
10
+ # configured access to. The nil-user case follows `response_filter_nil_user_strategy`.
11
+ #
12
+ # Each call is one read for the field.access audit event
13
+ # (Audit::FieldAccess): the record is named by the resource and the hash's
14
+ # `id`, the hidden, masked and unconfigured keys are listed, and the
15
+ # legacy on_field_filtered hook is called for every configured key, as
16
+ # before.
17
+ #
18
+ # @example
19
+ # filtered = FiltersResponseAttributes.call(
20
+ # attributes: { name: 'John', email: 'john@example.com' },
21
+ # resource_name: 'Customer'
22
+ # )
23
+ #
24
+ # @api public
25
+ class FiltersResponseAttributes
26
+ # Filters a response hash by the user's field-level read access (does not mutate it).
27
+ # @param attributes [Hash]
28
+ # @param resource_name [String] the model name the fields belong to
29
+ # @param user [Object, nil] defaults to the current user
30
+ # @return [Hash]
31
+ def self.call(attributes:, resource_name:, user: nil)
32
+ new(attributes: attributes, resource_name: resource_name, user: user).call
33
+ end
34
+
35
+ def initialize(attributes:, resource_name:, user: nil)
36
+ @attributes = attributes
37
+ @resource_name = resource_name.to_s
38
+ @user = user || PermissionForge.current_user
39
+ end
40
+
41
+ # @return [Hash] a new hash without the fields the user may not read, masked where required
42
+ def call
43
+ PermissionForge::Audit::FieldAccess.read(@attributes, @resource_name, record_id: record_id) do |frame|
44
+ @frame = frame
45
+ next handle_nil_user if @user.blank?
46
+
47
+ # No early return on blank field_permissions: an authenticated user with no
48
+ # configured access to the resource must still be filtered (fail closed),
49
+ # not handed the raw attributes.
50
+ filter_and_mask
51
+ end
52
+ end
53
+
54
+ UNCONFIGURED_FIELD_STRATEGIES = {
55
+ pass_through: ->(_resource_name, _key, value) { [true, value] },
56
+ warn: lambda { |resource_name, key, value|
57
+ Rails.logger.warn("PermissionForge: unconfigured field '#{key}' on #{resource_name} passed through unmasked")
58
+ [true, value]
59
+ },
60
+ mask_all: ->(_resource_name, _key, value) { [true, MaskValue.call(value, strategy: 'full')] },
61
+ block: ->(_resource_name, _key, _value) { [false, nil] }
62
+ }.freeze
63
+
64
+ private
65
+
66
+ # When there is no current user we cannot evaluate permissions. The behavior
67
+ # is explicit and configurable via `response_filter_nil_user_strategy`:
68
+ # :bypass — return raw attributes (system/console context; default, backward-compatible)
69
+ # :block — return {} (strict fail-closed)
70
+ def handle_nil_user
71
+ if PermissionForge.configuration.response_filter_nil_user_strategy == :block
72
+ note_all(:hidden)
73
+ {}
74
+ else
75
+ note_all(:bypassed)
76
+ @attributes
77
+ end
78
+ end
79
+
80
+ # Without a user every key is decided the same way (the hook was never
81
+ # called for these).
82
+ def note_all(action)
83
+ @attributes.each_key { |key| @frame.add(@resource_name, key, action, nil, hook: false) } if @frame
84
+ end
85
+
86
+ # The id in the hash, when it has one (the hash's own key, not a reader).
87
+ def record_id
88
+ return unless @attributes.respond_to?(:key?)
89
+
90
+ @attributes.key?(:id) ? @attributes[:id] : @attributes['id']
91
+ end
92
+
93
+ def filter_and_mask
94
+ @result = {}
95
+ @attributes.each { |key, value| process_field(key, value) }
96
+ @result
97
+ end
98
+
99
+ def process_field(key, value)
100
+ field = field_permissions[key.to_sym]
101
+
102
+ if field.nil?
103
+ include_field, masked_value = handle_unconfigured_field(key, value)
104
+ @result[key] = masked_value if include_field
105
+ @frame&.add(@resource_name, key, :unconfigured, @user, hook: false)
106
+ else
107
+ apply_configured_field(key, value, field)
108
+ end
109
+ end
110
+
111
+ def apply_configured_field(key, value, field)
112
+ unless field[:read_access]
113
+ notify_field_filtered(key, :hidden)
114
+ return
115
+ end
116
+
117
+ if field[:mask_access]
118
+ @result[key] = MaskValue.call(value, strategy: field[:mask_strategy] || 'full')
119
+ notify_field_filtered(key, :masked)
120
+ else
121
+ @result[key] = value
122
+ notify_field_filtered(key, :passed)
123
+ end
124
+ end
125
+
126
+ def notify_field_filtered(field_name, action)
127
+ @frame&.add(@resource_name, field_name, action, @user)
128
+ end
129
+
130
+ def handle_unconfigured_field(key, value)
131
+ strategy_key = PermissionForge.configuration.unconfigured_field_strategy
132
+ # Unknown strategy keys fall back to :block (fail closed), never pass-through.
133
+ strategy = UNCONFIGURED_FIELD_STRATEGIES.fetch(strategy_key, UNCONFIGURED_FIELD_STRATEGIES[:block])
134
+ strategy.call(@resource_name, key, value)
135
+ end
136
+
137
+ # The user's field entries for the resource: those of the model the
138
+ # name names and of its STI ancestors (ResourceChain); {} without any.
139
+ def field_permissions
140
+ @field_permissions ||= PermissionForge::ResourceChain.fields(@user, PermissionForge::ResourceChain.names_for(@resource_name))
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Services
5
+ class GdprFieldAuditor
6
+ # Plain-text rendering of a GdprFieldAuditor report for terminals and
7
+ # logs. One line per finding, whatever its type, so the count in the
8
+ # header is the number of lines below it.
9
+ #
10
+ # @api private
11
+ class TextReport
12
+ CLEAN = 'All GDPR-sensitive fields are properly masked.'
13
+
14
+ LINES = {
15
+ unmasked_field: lambda { |finding|
16
+ "[UNMASKED] #{finding[:resource_name]}.#{finding[:field_name]} " \
17
+ "— matches pattern '#{finding[:suggested_strategy]}'"
18
+ },
19
+ unmapped_column: lambda { |finding|
20
+ "[UNMAPPED] #{finding[:resource]}.#{finding[:column]} " \
21
+ "— matches pattern '#{finding[:pattern_matched]}', no active ResourceField"
22
+ },
23
+ implicit_virtual_grant: lambda { |finding|
24
+ "[VIRTUAL] #{finding[:permission_set]} › #{finding[:resource_name]}.#{finding[:field_name]} " \
25
+ "— matches a GDPR pattern (suggested mask: '#{finding[:suggested_strategy]}'), " \
26
+ 'no active ResourceField, readable and editable by default'
27
+ },
28
+ view_all_on_pii_resource: lambda { |finding|
29
+ "[VIEW ALL] #{finding[:permission_set]} › #{finding[:resource_name]} " \
30
+ "— #{finding[:bypasses].join(' + ')} skips row-level rules; GDPR-sensitive fields " \
31
+ "on this resource: #{finding[:pii_fields].join(', ')}"
32
+ },
33
+ cross_set_unmasking: lambda { |finding|
34
+ "[OVERRIDES MASK] #{finding[:permission_set]} › #{finding[:resource_name]}.#{finding[:field_name]} " \
35
+ '— unmasked here, masked in another Permission Set; a person in both sees the full value'
36
+ }
37
+ }.freeze
38
+
39
+ # A finding type added to the auditor without a line here still prints.
40
+ FALLBACK = lambda { |finding|
41
+ "[#{finding[:type].to_s.upcase.tr('_', ' ')}] " \
42
+ "#{finding.except(:type).map { |key, value| "#{key}: #{value}" }.join(', ')}"
43
+ }
44
+
45
+ def initialize(report)
46
+ @report = report
47
+ end
48
+
49
+ def to_s
50
+ return CLEAN if @report.empty?
51
+
52
+ ["GDPR audit found #{@report.size} issue(s):", *@report.map { |finding| " #{line(finding)}" }].join("\n")
53
+ end
54
+
55
+ private
56
+
57
+ def line(finding)
58
+ LINES.fetch(finding[:type], FALLBACK).call(finding)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end