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,3 @@
1
+ module PermissionForge
2
+ VERSION = '0.14.1'.freeze
3
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ class VirtualFields
5
+ # Finds virtual (non-column) fields a model exposes without declaring them
6
+ # through extra_fields_method: attribute-API attributes and store accessors.
7
+ #
8
+ # It only reports names. Whether they are listed is VirtualFields'
9
+ # decision (config.virtual_field_discovery), and a discovered name never
10
+ # gets implicit access — ExtraFieldDefaults reads declared names only.
11
+ #
12
+ # Nested-attribute names (`<association>_attributes`) are left out on
13
+ # purpose: PermitsAttributes filters those payloads by the nested model's
14
+ # own Resource, and a field row on the parent would change that (an
15
+ # Editable row passes the payload unfiltered, any other level does not
16
+ # block it), so a level control for them would not mean what it says.
17
+ #
18
+ # @example
19
+ # PermissionForge::VirtualFields::Discovery.call(Customer) # => ["nickname", "theme"]
20
+ #
21
+ # @api private
22
+ class Discovery
23
+ # One private method per source; add a source by adding a name here.
24
+ SOURCES = %i[attribute_api_names store_accessor_names].freeze
25
+
26
+ def self.call(klass) = new(klass).call
27
+
28
+ def initialize(klass)
29
+ @klass = klass
30
+ end
31
+
32
+ # @return [Array<String>] discovered names, never a column
33
+ def call
34
+ SOURCES.flat_map { |source| send(source) }.uniq - @klass.column_names
35
+ rescue StandardError
36
+ []
37
+ end
38
+
39
+ private
40
+
41
+ # `attribute :nickname, :string` with no column behind it.
42
+ def attribute_api_names
43
+ @klass.attribute_names
44
+ end
45
+
46
+ # `store_accessor :settings, :theme`, by reader name.
47
+ def store_accessor_names
48
+ return [] unless @klass.respond_to?(:stored_attributes)
49
+
50
+ @klass.stored_attributes.flat_map do |store, keys|
51
+ keys.flat_map { |key| accessor_names(store, key) }
52
+ end
53
+ end
54
+
55
+ # stored_attributes records keys, not reader names: with prefix: true the
56
+ # reader is `<store>_<key>`, with suffix: true `<key>_<store>`. Every
57
+ # spelling that is a method counts (a bare key can also be a column,
58
+ # which #call removes). Custom prefixes are not recorded anywhere, so
59
+ # those accessors are not found.
60
+ def accessor_names(store, key)
61
+ [key.to_s, "#{store}_#{key}", "#{key}_#{store}"].select { |name| @klass.method_defined?(name) }
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ # The virtual (non-column) fields of one model: names the host declared
5
+ # through extra_fields_method, plus discovered ones while
6
+ # config.virtual_field_discovery is on.
7
+ #
8
+ # The one place extra_fields_method is read. The engine reads it through
9
+ # VirtualFields.extra_names, which raises what the host method raises: a
10
+ # broken method must not silently change permissions. Read models, audits
11
+ # and reader wrapping go through #extra, which logs the error and carries on
12
+ # with no declared names.
13
+ #
14
+ # @example
15
+ # PermissionForge::VirtualFields.extra_names(Customer) # => ["full_address"]
16
+ #
17
+ # fields = PermissionForge::VirtualFields.new(Customer)
18
+ # fields.names # => ["full_address", "nickname"]
19
+ # fields.implicit_grant?('full_address') # => true under extra_fields_default_access = :grant
20
+ # fields.implicit_grant?('nickname') # => false: discovered, never granted
21
+ #
22
+ # @api public
23
+ class VirtualFields
24
+ # Names the model declares through extra_fields_method, as Strings and
25
+ # exactly as declared (they may include column names). Empty for a class
26
+ # without the method, and for nil.
27
+ #
28
+ # @raise whatever the host's method raises
29
+ # @return [Array<String>]
30
+ def self.extra_names(klass, configuration = PermissionForge.configuration)
31
+ method = configuration.extra_fields_method
32
+ method && klass.respond_to?(method) ? Array(klass.public_send(method)).map(&:to_s) : []
33
+ end
34
+
35
+ # The error the host's method raised when #extra read it, nil otherwise.
36
+ attr_reader :extra_error
37
+
38
+ def initialize(klass, configuration = PermissionForge.configuration)
39
+ @klass = klass
40
+ @configuration = configuration
41
+ end
42
+
43
+ # VirtualFields.extra_names, except that an error is logged and gives [].
44
+ def extra
45
+ @extra ||= self.class.extra_names(@klass, @configuration)
46
+ rescue StandardError => e
47
+ @extra = extra_unreadable(e)
48
+ end
49
+
50
+ # Declared names that are neither a column nor an ignored field.
51
+ def declared
52
+ @declared ||= extra - excluded
53
+ end
54
+
55
+ # Listed only while discovery is on; never implicitly granted.
56
+ def discovered
57
+ @discovered ||= @configuration.virtual_field_discovery ? discoverable - excluded - extra : []
58
+ end
59
+
60
+ # @return [Array<String>] the declared and discovered virtual field names
61
+ def names
62
+ @names ||= (declared + discovered).uniq
63
+ end
64
+
65
+ # Names that get the implicit read + edit entry from ExtraFieldDefaults
66
+ # when no active ResourceField row configures them: every declared name
67
+ # under extra_fields_default_access = :grant, none otherwise.
68
+ def implicit
69
+ @configuration.grant_extra_fields_by_default? ? extra : []
70
+ end
71
+
72
+ def implicit_grant?(name)
73
+ implicit.include?(name.to_s)
74
+ end
75
+
76
+ # Whether the model has `name` as a virtual field, listed or not (a
77
+ # field configured while discovery was on stays a virtual field).
78
+ def include?(name)
79
+ name = name.to_s
80
+ @klass.column_names.exclude?(name) && (extra.include?(name) || discoverable.include?(name))
81
+ end
82
+
83
+ private
84
+
85
+ def discoverable
86
+ @discoverable ||= Discovery.call(@klass)
87
+ end
88
+
89
+ def excluded
90
+ @klass.column_names + Array(@configuration.ignored_fields).map(&:to_s)
91
+ end
92
+
93
+ # Keeps the error for callers that must say what it costs them (reader
94
+ # wrapping), logs it, and gives the empty list #extra then returns.
95
+ def extra_unreadable(error)
96
+ @extra_error = error
97
+ Rails.logger&.warn("[PermissionForge] #{@klass}.#{@configuration.extra_fields_method} raised " \
98
+ "#{error.class}: #{error.message}. Carrying on as if the model declared no virtual fields.")
99
+ []
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,464 @@
1
+ require 'active_support'
2
+ require 'active_record'
3
+
4
+ require_relative 'permission_forge/version'
5
+ require_relative 'permission_forge/deprecation'
6
+ require_relative 'permission_forge/configuration/gdpr_settings'
7
+ require_relative 'permission_forge/configuration/masked_attributes_settings'
8
+ require_relative 'permission_forge/configuration/permits_attributes_settings'
9
+ require_relative 'permission_forge/configuration/query_scoping_settings'
10
+ require_relative 'permission_forge/configuration/admin_ui_settings'
11
+ require_relative 'permission_forge/configuration/virtual_attributes_settings'
12
+ require_relative 'permission_forge/configuration/controller_action_settings'
13
+ require_relative 'permission_forge/configuration/cache_settings'
14
+ require_relative 'permission_forge/configuration/audit_settings'
15
+ require_relative 'permission_forge/configuration/strategy_values'
16
+ require_relative 'permission_forge/configuration/presets'
17
+ require_relative 'permission_forge/configuration'
18
+
19
+ # Entry point of the gem: configuration, the current user, the cache and the table prefix.
20
+ #
21
+ # @example
22
+ # PermissionForge.configure do |config|
23
+ # config.current_user_resolver = -> { Current.user }
24
+ # end
25
+ #
26
+ # @api public
27
+ module PermissionForge
28
+ # Raised by Level 2 detection (config.unscoped_query_detection = :raise) when
29
+ # a protected model is queried outside a permission-scoped path.
30
+ #
31
+ # @api public
32
+ class UnscopedQueryError < StandardError; end
33
+
34
+ # Raised by Concerns::ControllerActionGuard when the current user may not run
35
+ # the requested controller action. The guard never renders — rescue this in
36
+ # the host app (rescue_from) and answer 403.
37
+ #
38
+ # `reason` is the Authorizer's reason symbol, so a handler can tell "nobody
39
+ # is signed in" (:nil_user_denied — send to login) from a real denial.
40
+ #
41
+ # @api public
42
+ class ActionNotAuthorizedError < StandardError
43
+ # user: who was refused (nil for no user); record: the record the action
44
+ # was about, when the caller knows it (the guard runs before the action
45
+ # loads one, so it passes none).
46
+ attr_reader :resource, :action, :controller_path, :reason, :user, :record
47
+
48
+ def initialize(resource:, action:, controller_path:, reason: nil, user: nil, record: nil) # rubocop:disable Metrics/ParameterLists
49
+ @resource = resource
50
+ @action = action
51
+ @controller_path = controller_path
52
+ @reason = reason
53
+ @user = user
54
+ @record = record
55
+ super("Not authorized to run '#{action}' on #{resource} (#{controller_path})")
56
+ end
57
+ end
58
+
59
+ # Locales the gem ships translation files for. Used as the fallback when
60
+ # config.available_locales is not set, so the admin UI never reflects
61
+ # locales registered by the host app (e.g. Faker) that PermissionForge
62
+ # has no translations for.
63
+ BUNDLED_LOCALES = %i[en sq mk].freeze
64
+
65
+ # A Resource name (Resource#name, a controller's declared resource): a Ruby
66
+ # constant path. The one definition — Resource::NAME_FORMAT and
67
+ # ControllerActions::RESOURCE_FORMAT point here.
68
+ RESOURCE_NAME_FORMAT = /\A[A-Z][A-Za-z0-9_]*(::[A-Z][A-Za-z0-9_]*)*\z/
69
+
70
+ # The ActiveRecord models, in app/models/permission_forge. They are
71
+ # autoloaded (and in development reloaded) by the host application, not
72
+ # required by the gem, so a class body reads the configuration — the user
73
+ # class of an assignment, the audit user class, the tenant model — after the
74
+ # host's config/initializers have run, and there is nothing to rebind.
75
+ MODEL_NAMES = %i[
76
+ PermissionSet PermissionSetUser PermissionSetGroup PermissionSetGroupUser
77
+ Resource ResourceField ResourceAction PermissionSetRule AuditEvent
78
+ ].freeze
79
+
80
+ # ExecutionState slot (fiber-local): when truthy, Level 1 auto-scoping and
81
+ # Level 2 detection are suspended for the running fiber. Set via
82
+ # without_permission_scope.
83
+ SKIP_SCOPE_KEY = :pf_skip_auto_scope
84
+
85
+ # Guards @permissions_cache_version increments; reads are lock-free.
86
+ PERMISSIONS_CACHE_VERSION_MUTEX = Mutex.new
87
+
88
+ class << self
89
+ # The models can only be resolved once the Rails application has set up
90
+ # its autoloader, which happens after every file in config/initializers
91
+ # has run (Rails 7.1+). Before that — the top level of an initializer, or
92
+ # no Rails application at all — say what to do instead of "uninitialized
93
+ # constant". Zeitwerk registers the autoloads itself, so this is never
94
+ # reached for a model name after boot.
95
+ def const_missing(name)
96
+ return super unless MODEL_NAMES.include?(name)
97
+
98
+ raise NameError.new(
99
+ "PermissionForge::#{name} is autoloaded by the Rails application once it has booted, so it cannot be " \
100
+ 'referenced before that: not from the top level of a file in config/initializers, an engine or railtie ' \
101
+ 'initializer or a file required from config/application.rb, nor outside a Rails application. Reference ' \
102
+ 'it from a method (a model, a controller, a job, a rake task). To change the class — include a module, ' \
103
+ 'add a callback — use Rails.application.config.to_prepare or ' \
104
+ "Rails.autoloaders.main.on_load('PermissionForge::#{name}'), which run again after every code reload; " \
105
+ 'config.after_initialize runs once, so use it only to read.',
106
+ name
107
+ )
108
+ end
109
+
110
+ # @return [Configuration] the settings, created on first use
111
+ def configuration
112
+ @configuration ||= Configuration.new
113
+ end
114
+
115
+ # Configure in config/initializers: the models read the configuration as
116
+ # they load, right after. A setting a loaded model took is compared
117
+ # afterwards and a difference logged (ModelBindings).
118
+ def configure
119
+ yield(configuration)
120
+ configuration.reset_cache_store!
121
+ ModelBindings.warn!(configuration)
122
+ end
123
+
124
+ # Replaces the configuration with a fresh default one (tests and console).
125
+ # @return [void]
126
+ def reset!
127
+ @configuration = Configuration.new
128
+ bump_permissions_cache_version!
129
+ end
130
+
131
+ # Process-local generation counter for cached permission hashes. Instances
132
+ # memoize their permissions_hash keyed by this value; bumping it (on any
133
+ # invalidation, regeneration, or cache-store swap) makes every memo in the
134
+ # process re-read from the cache store on next access. Cross-process
135
+ # coherence is unchanged — it still comes from the shared cache store.
136
+ def permissions_cache_version
137
+ @permissions_cache_version || 0
138
+ end
139
+
140
+ # Retires every per-instance `permissions_hash` memo (they compare against this version).
141
+ # Called by `invalidate_permissions`, `regenerate_permissions` and a cache-store swap.
142
+ # @return [Integer] the new version
143
+ # @api private
144
+ def bump_permissions_cache_version!
145
+ PERMISSIONS_CACHE_VERSION_MUTEX.synchronize do
146
+ @permissions_cache_version = permissions_cache_version + 1
147
+ end
148
+ end
149
+
150
+ # @return [Object, nil] whatever `config.current_user_resolver` answers
151
+ def current_user
152
+ configuration.current_user_resolver.call
153
+ end
154
+
155
+ # @return [Cache::Base] the permission cache store (`config.cache_store`)
156
+ def cache
157
+ configuration.cache_store
158
+ end
159
+
160
+ # Id of the tenant acts_as_tenant has made ambient, when tenancy is on:
161
+ # the tenant a permission hash is built and cached for.
162
+ def ambient_tenant_id
163
+ return unless configuration.tenant_enabled? && defined?(ActsAsTenant)
164
+
165
+ ActsAsTenant.current_tenant&.id
166
+ end
167
+
168
+ # The tenant acts_as_tenant has made ambient (nil: none, or tenancy is off).
169
+ # Together with with_ambient_tenant it carries a request's tenant into work
170
+ # that runs after the request's own wrappers have ended (a streamed body).
171
+ def ambient_tenant
172
+ return unless configuration.tenant_enabled? && defined?(ActsAsTenant)
173
+
174
+ ActsAsTenant.current_tenant
175
+ end
176
+
177
+ # Runs the block with `tenant` ambient. Does nothing when there is none to
178
+ # pin, or it is the ambient one already. Restores the request-level tenant
179
+ # as it was (not acts_as_tenant's with_tenant, which writes the tenant it
180
+ # resolved — test or default included — back into ActsAsTenant::Current).
181
+ def with_ambient_tenant(tenant)
182
+ return yield unless tenant && configuration.tenant_enabled? && defined?(ActsAsTenant)
183
+ return yield if ActsAsTenant.current_tenant == tenant
184
+
185
+ previous = ActsAsTenant::Current.current_tenant
186
+ ActsAsTenant.current_tenant = tenant
187
+ begin
188
+ yield
189
+ ensure
190
+ ActsAsTenant::Current.current_tenant = previous
191
+ end
192
+ end
193
+
194
+ # @return [String] the table prefix, `permission_forge_` unless configured
195
+ def table_name_prefix
196
+ configuration.table_name_prefix
197
+ end
198
+
199
+ # Escape hatch for intentional unscoped access (admin panels, reports,
200
+ # background maintenance). Inside the block, Level 1 auto-scoping and
201
+ # Level 2 detection are suspended on the running fiber (not in another
202
+ # fiber, an Enumerator's included: ExecutionState), and with nobody
203
+ # signed in find_by, find_by! and permission_scoped apply no rules,
204
+ # whatever nil_user_strategy says (rule.bypassed, scope_suspended); a
205
+ # signed-in user's find_by and permission_scoped still apply theirs. So
206
+ # inside the block nobody signed in is trusted more than a signed-in
207
+ # user: wrap trusted code only (jobs, seeds, rake tasks, sign-in lookups),
208
+ # never code an anonymous request reaches.
209
+ # Nesting-safe: restores the previous flag value on exit.
210
+ #
211
+ # PermissionForge.without_permission_scope { Customer.where(...) }
212
+ def without_permission_scope(&)
213
+ ExecutionState.with(SKIP_SCOPE_KEY, true, &)
214
+ end
215
+
216
+ # @return [Boolean] whether permission scoping is currently suspended on
217
+ # this thread (internal builds and without_permission_scope blocks).
218
+ def scope_suspended?
219
+ !!ExecutionState[SKIP_SCOPE_KEY]
220
+ end
221
+
222
+ # Names who acts for every audit event recorded inside the block — a job,
223
+ # a rake task, a service run on someone's behalf — and where it comes
224
+ # from. Wins over the request context and PermissionForge.current_user.
225
+ #
226
+ # PermissionForge.with_audit_actor(user: admin, source: 'job') { cleaner.call }
227
+ # PermissionForge.with_audit_actor(label: ENV['ACTOR'], source: 'rake', reason: 'TCK-12') { ... }
228
+ #
229
+ # @param user [Object, nil] the acting user (actor_type 'user' unless type: says otherwise)
230
+ # @param label [String, nil] the name recorded; required without a user
231
+ # @param type [String, nil] user, api, basic_auth, system (default: 'user'
232
+ # with a user, 'system' without)
233
+ # @param source [String, nil] admin_ui, api, rake, job, console, service, runtime
234
+ # @param reason [String, nil] stored on every event of the block
235
+ # @return the value of the block
236
+ def with_audit_actor(user: nil, label: nil, type: nil, source: nil, reason: nil, &)
237
+ actor = if user
238
+ Audit::Actor.for_user(user, label: label, type: type || 'user')
239
+ elsif label || type
240
+ Audit::Actor.new(type: type || 'system', label: label)
241
+ end
242
+ Audit::Context.with(actor: actor, source: source, reason: reason, &)
243
+ end
244
+
245
+ # Returns an array of regexes derived from the configured GDPR field patterns,
246
+ # suitable for appending to Rails.application.config.filter_parameters.
247
+ #
248
+ # Usage (manual):
249
+ # Rails.application.config.filter_parameters += PermissionForge.gdpr_filter_parameters
250
+ #
251
+ # Or set config.filter_parameters_integration = true for automatic injection.
252
+ def gdpr_filter_parameters
253
+ return [] unless configuration.gdpr_defaults
254
+
255
+ configuration.gdpr_field_patterns.keys
256
+ end
257
+ end
258
+ end
259
+
260
+ require_relative 'permission_forge/execution_state'
261
+ require_relative 'permission_forge/model_bindings'
262
+ require_relative 'permission_forge/cache/base'
263
+ require_relative 'permission_forge/cache/memory_cache'
264
+ require_relative 'permission_forge/cache/null_cache'
265
+ require_relative 'permission_forge/cache/handled_errors'
266
+ require_relative 'permission_forge/cache/rails_cache'
267
+ require_relative 'permission_forge/cache/failure'
268
+ require_relative 'permission_forge/cache/backlog'
269
+ require_relative 'permission_forge/cache/invalidation'
270
+ require_relative 'permission_forge/cache/bulk_invalidation'
271
+ require_relative 'permission_forge/cache/user_keys'
272
+ require_relative 'permission_forge/cache/holder_invalidation'
273
+ require_relative 'permission_forge/cache/variant_removal'
274
+ require_relative 'permission_forge/cache/withheld_notice'
275
+ require_relative 'permission_forge/cache/rebuild'
276
+ require_relative 'permission_forge/open_transaction'
277
+ require_relative 'permission_forge/cache/settlement'
278
+
279
+ # Audit event log
280
+ require_relative 'permission_forge/audit/event_catalog'
281
+ require_relative 'permission_forge/audit/actor'
282
+ require_relative 'permission_forge/audit/source'
283
+ require_relative 'permission_forge/audit/context'
284
+ require_relative 'permission_forge/audit/failure'
285
+ require_relative 'permission_forge/audit/column_values'
286
+ require_relative 'permission_forge/audit/recorder'
287
+ require_relative 'permission_forge/audit/subscribers'
288
+ require_relative 'permission_forge/audit/log_access'
289
+ require_relative 'permission_forge/audit/event_query'
290
+ require_relative 'permission_forge/audit/runtime_emission'
291
+ require_relative 'permission_forge/audit'
292
+ require_relative 'permission_forge/audit/gate'
293
+ require_relative 'permission_forge/audit/runtime'
294
+ require_relative 'permission_forge/audit/field_access'
295
+ require_relative 'permission_forge/audit/subscribers/base'
296
+ require_relative 'permission_forge/audit/subscribers/legacy_hooks'
297
+ require_relative 'permission_forge/audit/subscribers/log'
298
+ require_relative 'permission_forge/audit/actor_resolver'
299
+ require_relative 'permission_forge/audit/audited_views'
300
+ require_relative 'permission_forge/audit/request_context'
301
+ require_relative 'permission_forge/audit/detached'
302
+ require_relative 'permission_forge/audit/refusals'
303
+ require_relative 'permission_forge/audit/job_context'
304
+ require_relative 'permission_forge/audit/settings_observer'
305
+ require_relative 'permission_forge/audit/boot_warnings'
306
+
307
+ require_relative 'permission_forge/resource_chain'
308
+ require_relative 'permission_forge/virtual_fields'
309
+ require_relative 'permission_forge/virtual_fields/discovery'
310
+
311
+ require_relative 'permission_forge/concerns/has_active_scope'
312
+ require_relative 'permission_forge/concerns/has_tenant_scoping'
313
+ require_relative 'permission_forge/concerns/has_uuid'
314
+ require_relative 'permission_forge/concerns/has_audit_trail'
315
+ require_relative 'permission_forge/invalidation_batch'
316
+ require_relative 'permission_forge/concerns/invalidates_in_batch'
317
+ require_relative 'permission_forge/concerns/has_permission_set_users'
318
+ require_relative 'permission_forge/concerns/validates_dependency'
319
+ require_relative 'permission_forge/concerns/validates_column_lengths'
320
+ require_relative 'permission_forge/concerns/permissions_regenerator'
321
+ require_relative 'permission_forge/concerns/invalidates_managed_actions'
322
+ require_relative 'permission_forge/concerns/records_audit_events'
323
+ require_relative 'permission_forge/concerns/audited_collection'
324
+ require_relative 'permission_forge/conditional_sets'
325
+ require_relative 'permission_forge/concerns/permissions_memo'
326
+ require_relative 'permission_forge/concerns/permissions_per_context'
327
+ require_relative 'permission_forge/concerns/permission_cache_accessor'
328
+ require_relative 'permission_forge/concerns/resource_permission_checker'
329
+ require_relative 'permission_forge/concerns/field_mask_checker'
330
+ require_relative 'permission_forge/concerns/permissioned_user'
331
+ require_relative 'permission_forge/concerns/auto_permission_scoping'
332
+ require_relative 'permission_forge/concerns/permission_scoped_findable'
333
+ require_relative 'permission_forge/concerns/masked_attributes'
334
+
335
+ require_relative 'permission_forge/operators/constants'
336
+ require_relative 'permission_forge/operators/validations'
337
+ require_relative 'permission_forge/operators/value_casters'
338
+ require_relative 'permission_forge/operators/rule_column'
339
+ require_relative 'permission_forge/operators/value_typing'
340
+ require_relative 'permission_forge/services/mask_value'
341
+
342
+ # The models are not required here: they live in app/models/permission_forge
343
+ # and are autoloaded by the host application (see MODEL_NAMES above), so
344
+ # their class bodies read the configuration after config/initializers.
345
+
346
+ require_relative 'permission_forge/operators/registry'
347
+
348
+ require_relative 'permission_forge/controller_actions'
349
+ require_relative 'permission_forge/controller_actions/mapping'
350
+ require_relative 'permission_forge/controller_actions/managed_index'
351
+ require_relative 'permission_forge/controller_actions/authorizer'
352
+ require_relative 'permission_forge/concerns/controller_action_guard'
353
+
354
+ require_relative 'permission_forge/api/serializers/base_serializer'
355
+ require_relative 'permission_forge/api/serializers/permission_set_serializer'
356
+ require_relative 'permission_forge/api/serializers/resource_serializer'
357
+ require_relative 'permission_forge/api/serializers/resource_field_serializer'
358
+ require_relative 'permission_forge/api/serializers/resource_action_serializer'
359
+ require_relative 'permission_forge/api/serializers/permission_set_rule_serializer'
360
+ require_relative 'permission_forge/api/serializers/permission_set_user_serializer'
361
+ require_relative 'permission_forge/api/serializers/audit_event_serializer'
362
+ require_relative 'permission_forge/api/serializers/permission_set_group_serializer'
363
+ require_relative 'permission_forge/api/serializers/permission_set_group_user_serializer'
364
+
365
+ require_relative 'permission_forge/services/concerns/clonable'
366
+ require_relative 'permission_forge/services/concerns/audited_operation'
367
+ require_relative 'permission_forge/services/concerns/across_tenants'
368
+ require_relative 'permission_forge/services/bulk_permission_set_assignment'
369
+ require_relative 'permission_forge/services/consent_withdrawal_handler'
370
+ require_relative 'permission_forge/services/expired_assignment_cleaner'
371
+ require_relative 'permission_forge/services/permission_cache_clearer'
372
+ require_relative 'permission_forge/services/permission_set_condition_evaluator'
373
+ require_relative 'permission_forge/services/permission_set_cloner'
374
+ require_relative 'permission_forge/services/permission_set_group_cloner'
375
+ require_relative 'permission_forge/services/permission_rule_applier'
376
+ require_relative 'permission_forge/services/resource_with_fields_builder'
377
+ require_relative 'permission_forge/services/dashboard_stats'
378
+ require_relative 'permission_forge/services/extra_field_defaults'
379
+ require_relative 'permission_forge/services/field_permission_merger'
380
+ require_relative 'permission_forge/services/filters_response_attributes'
381
+ require_relative 'permission_forge/services/attribute_access_guard'
382
+ require_relative 'permission_forge/services/gdpr_field_auditor'
383
+ require_relative 'permission_forge/services/permission_hash_builder'
384
+ require_relative 'permission_forge/services/permits_attributes'
385
+ require_relative 'permission_forge/services/permits_attributes/nested_records'
386
+ require_relative 'permission_forge/services/data_subject_erasure'
387
+ require_relative 'permission_forge/services/host_key_guard'
388
+ require_relative 'permission_forge/services/stale_assignment_auditor'
389
+ require_relative 'permission_forge/services/audit_event_purger'
390
+ require_relative 'permission_forge/rake_audit'
391
+ require_relative 'permission_forge/services/resource_field_generator'
392
+ require_relative 'permission_forge/services/unscoped_query_detector'
393
+ require_relative 'permission_forge/services/rls_operator_sql'
394
+ require_relative 'permission_forge/services/rls_policy_generator'
395
+ require_relative 'permission_forge/rls'
396
+
397
+ # Admin UI support (read models + batched edits for the built-in admin screens)
398
+ require_relative 'permission_forge/admin/field_level'
399
+ require_relative 'permission_forge/admin/sensitive_columns'
400
+ require_relative 'permission_forge/admin/model_catalog'
401
+ require_relative 'permission_forge/admin/person_label'
402
+ require_relative 'permission_forge/admin/people_candidates'
403
+ require_relative 'permission_forge/admin/people_directory'
404
+ require_relative 'permission_forge/admin/record_id'
405
+ require_relative 'permission_forge/admin/editable_lock'
406
+ require_relative 'permission_forge/admin/expiry_presets'
407
+ require_relative 'permission_forge/admin/effective_access'
408
+ require_relative 'permission_forge/admin/effective_fields'
409
+ require_relative 'permission_forge/admin/settings_values'
410
+ require_relative 'permission_forge/admin/settings_catalog'
411
+ require_relative 'permission_forge/admin/csv'
412
+ require_relative 'permission_forge/admin/last_changes'
413
+ require_relative 'permission_forge/admin/audit_trail'
414
+ require_relative 'permission_forge/admin/audit_log_days'
415
+ require_relative 'permission_forge/admin/audit_log'
416
+ require_relative 'permission_forge/admin/audit_name_history'
417
+ require_relative 'permission_forge/admin/audit_lookups'
418
+ require_relative 'permission_forge/admin/exports'
419
+ require_relative 'permission_forge/admin/exports/document'
420
+ require_relative 'permission_forge/admin/exports/events'
421
+ require_relative 'permission_forge/admin/exports/holdings'
422
+ require_relative 'permission_forge/admin/attention_items'
423
+ require_relative 'permission_forge/admin/change_set_parts/context'
424
+ require_relative 'permission_forge/admin/change_set_parts/values'
425
+ require_relative 'permission_forge/admin/change_set_parts/base'
426
+ require_relative 'permission_forge/admin/change_set_parts/set_attrs'
427
+ require_relative 'permission_forge/admin/change_set_parts/resource_edit'
428
+ require_relative 'permission_forge/admin/change_set_parts/resource_flags'
429
+ require_relative 'permission_forge/admin/change_set_parts/field_edit'
430
+ require_relative 'permission_forge/admin/change_set_parts/field_levels'
431
+ require_relative 'permission_forge/admin/change_set_parts/new_fields'
432
+ require_relative 'permission_forge/admin/change_set_parts/actions'
433
+ require_relative 'permission_forge/admin/change_set_parts/new_actions'
434
+ require_relative 'permission_forge/admin/change_set'
435
+ require_relative 'permission_forge/admin/field_rows'
436
+ require_relative 'permission_forge/admin/field_detail'
437
+ require_relative 'permission_forge/admin/audit_event_detail'
438
+ require_relative 'permission_forge/admin/rule_builder'
439
+ require_relative 'permission_forge/admin/field_across_sets'
440
+ require_relative 'permission_forge/admin/controller_catalog'
441
+ require_relative 'permission_forge/admin/action_across_sets'
442
+ require_relative 'permission_forge/admin/action_rows'
443
+ require_relative 'permission_forge/admin/column_fields'
444
+ require_relative 'permission_forge/admin/conditions_form'
445
+ require_relative 'permission_forge/admin/permission_set_factory'
446
+ require_relative 'permission_forge/admin/rule_form_input'
447
+ require_relative 'permission_forge/admin/rule_form'
448
+ require_relative 'permission_forge/admin/bulk_people_matcher'
449
+
450
+ # Security review (rake permission_forge:audit)
451
+ require_relative 'permission_forge/services/security_audit'
452
+ require_relative 'permission_forge/services/security_audit/base_check'
453
+ require_relative 'permission_forge/services/security_audit/config_check'
454
+ require_relative 'permission_forge/services/security_audit/privilege_check'
455
+ require_relative 'permission_forge/services/security_audit/assignment_check'
456
+ require_relative 'permission_forge/services/security_audit/rule_check'
457
+ require_relative 'permission_forge/services/security_audit/integrity_check'
458
+ require_relative 'permission_forge/services/security_audit/personal_data_check'
459
+ require_relative 'permission_forge/services/security_audit/action_check'
460
+ require_relative 'permission_forge/services/security_audit/audit_trail_check'
461
+ require_relative 'permission_forge/services/security_audit/user_model_check'
462
+ require_relative 'permission_forge/services/security_audit/text_report'
463
+
464
+ require_relative 'permission_forge/engine' if defined?(Rails::Engine)
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :permission_forge do
4
+ desc 'Report fields that match the personal-data patterns but are not masked, and matching model columns with no ResourceField'
5
+ task audit_masks: :environment do
6
+ report = PermissionForge::RakeAudit.reporting('permission_forge:audit_masks', counts: ->(r) { { findings: r.size } }) do
7
+ PermissionForge::Services::GdprFieldAuditor.call
8
+ end
9
+
10
+ puts PermissionForge::Services::GdprFieldAuditor::TextReport.new(report)
11
+ exit 1 if report.any?
12
+ end
13
+ end
14
+
15
+ namespace :permission_forge do
16
+ desc 'Read-only security review of the PermissionForge setup. FORMAT=json, FAIL_ON=critical|high|medium|low'
17
+ task audit: :environment do
18
+ report = PermissionForge::RakeAudit.reporting('permission_forge:audit', counts: :counts.to_proc) do
19
+ PermissionForge::Services::SecurityAudit.call
20
+ end
21
+
22
+ if ENV['FORMAT'] == 'json'
23
+ puts JSON.pretty_generate(report.to_h)
24
+ else
25
+ puts PermissionForge::Services::SecurityAuditChecks::TextReport.new(report)
26
+ end
27
+
28
+ fail_on = ENV.fetch('FAIL_ON', nil).presence
29
+ exit 1 if fail_on && report.at_least(fail_on).any?
30
+ end
31
+ end