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,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ class Configuration
5
+ # Options that only affect the built-in admin UI (never permission checks).
6
+ #
7
+ # @api public
8
+ module AdminUiSettings
9
+ # Lambda returning the model classes offered in "Add a resource".
10
+ # nil (default) eager-loads app/models and lists every concrete
11
+ # ActiveRecord model with a table, excluding PermissionForge's own.
12
+ # config.admin_model_resolver = -> { [Customer, Invoice, Deal] }
13
+ attr_accessor :admin_model_resolver
14
+
15
+ # Friendly labels for the session flags offered on the Conditions tab.
16
+ # Keys are the flag names your request_context_resolver puts in
17
+ # :session_flags. Flags already used by a Permission Set are always shown.
18
+ # config.admin_session_flags = { 'mfa_verified' => 'Signed in with two-factor' }
19
+ attr_accessor :admin_session_flags
20
+
21
+ # Assignments not reviewed for this many days appear under
22
+ # "Stale assignments" on the admin home page. Default: 90.
23
+ attr_accessor :admin_stale_assignment_days
24
+
25
+ # Name of the host app shown under the logo (e.g. "Acme CRM").
26
+ # nil (default) derives it from the Rails application class.
27
+ attr_accessor :admin_app_name
28
+
29
+ # Where the "Back to <app>" link under the logo points. A String, or a
30
+ # lambda receiving the view context (e.g. ->(view) { view.main_app.dashboard_path }).
31
+ # nil (default) uses the host app's root route, or "/".
32
+ attr_accessor :admin_back_url
33
+
34
+ # Base URL for "Configuration docs" links on the Settings page. The
35
+ # setting key is appended as an anchor.
36
+ attr_accessor :admin_docs_url
37
+
38
+ # What the admin screens and the admin JSON API do with a request that
39
+ # has no current tenant while tenancy is on (tenant_model):
40
+ #
41
+ # :deny — refuse it with a 403, recorded as admin.auth_failed /
42
+ # api.auth_failed, reason tenant_missing (default). With no
43
+ # tenant acts_as_tenant scopes nothing, so the request would
44
+ # read and change every tenant's permissions.
45
+ # :allow — serve it: it reads, edits and deletes existing records of
46
+ # every tenant. Creating a record still needs a tenant (the
47
+ # new row's tenant is required): with none, a create is
48
+ # refused as invalid ("must exist"), never stored tenant-less.
49
+ #
50
+ # A String is taken as its Symbol; any other value raises
51
+ # ConfigurationError (StrategyValues). Set the tenant in the controllers
52
+ # the engine inherits from (base_controller_class, api_base_controller_class)
53
+ # or in middleware.
54
+ attr_accessor :admin_tenantless_access
55
+
56
+ # Sets this group's defaults; called from Configuration#initialize.
57
+ # @return [void]
58
+ # @api private
59
+ def set_admin_ui_defaults
60
+ @admin_model_resolver = nil
61
+ @admin_session_flags = {}
62
+ @admin_stale_assignment_days = 90
63
+ @admin_app_name = nil
64
+ @admin_back_url = nil
65
+ @admin_docs_url = 'https://github.com/fetijashari/permission_forge/blob/main/docs/configuration.md'
66
+ @admin_tenantless_access = :deny
67
+ end
68
+
69
+ # @return [Boolean] whether an admin or admin API request may run with
70
+ # no current tenant. Only the exact :allow value allows.
71
+ def admin_tenantless_access_allowed?
72
+ admin_tenantless_access.to_s == 'allow'
73
+ end
74
+
75
+ # The admin base-controller settings still at the default
76
+ # ::ActionController::Base while tenancy is on: nothing the engine
77
+ # inherits from sets the tenant there (the API's only counts when the
78
+ # API is enabled).
79
+ #
80
+ # @return [Array<Symbol>] e.g. [:base_controller_class]
81
+ def default_admin_controllers_under_tenancy
82
+ return [] unless tenant_enabled?
83
+
84
+ names = { base_controller_class: base_controller_class }
85
+ names[:api_base_controller_class] = api_base_controller_class if api_enabled
86
+ names.select { |_, name| name.to_s.delete_prefix('::') == 'ActionController::Base' }.keys
87
+ end
88
+
89
+ # The boot warning for default_admin_controllers_under_tenancy, or nil.
90
+ #
91
+ # @return [String, nil]
92
+ def tenantless_admin_warning
93
+ names = default_admin_controllers_under_tenancy
94
+ return if names.empty?
95
+
96
+ consequence = if admin_tenantless_access_allowed?
97
+ 'admin_tenantless_access is :allow, so a request with no tenant reads and changes the ' \
98
+ 'existing records of every tenant.'
99
+ else
100
+ 'Every admin request with no tenant is refused (admin_tenantless_access :deny).'
101
+ end
102
+ '[PermissionForge] SECURITY warning: tenant_model is set but ActionController::Base, which sets no ' \
103
+ "tenant, is still the base of #{names.join(' and ')}. #{consequence} Point each at a controller " \
104
+ 'that sets the current tenant (or set the tenant in middleware).'
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ class Configuration
5
+ # Options for the audit event log ({PermissionForge::Audit}): what is
6
+ # recorded, who is recorded as the actor, what happens when a record
7
+ # cannot be written, and how long events are kept.
8
+ #
9
+ # The older options stay as they are: `audit_trail_enabled` and
10
+ # `audit_user_class` (created_by / updated_by on the live row),
11
+ # `on_rule_bypass` and `on_field_filtered` (callbacks).
12
+ #
13
+ # @api public
14
+ module AuditSettings
15
+ AUDIT_EVENTS = [:auto, true, false].freeze
16
+ CHANGE_FAILURE_STRATEGIES = %i[raise report log].freeze
17
+ HOOK_FAILURE_STRATEGIES = %i[report log raise].freeze
18
+
19
+ # The groups of runtime (access) events that can be stored, as named in
20
+ # the event catalogue (Audit::EventCatalog):
21
+ # denied — access.denied, action.denied
22
+ # bypass — rule.bypassed, query.unscoped
23
+ # field_read — field.access
24
+ # params_filtered — params.filtered, write.masked_skipped
25
+ # cache — permissions.rebuilt, permissions.invalidated
26
+ RUNTIME_GROUPS = %i[denied bypass field_read params_filtered cache].freeze
27
+
28
+ # Whether audit events are stored in the audit_events table.
29
+ # :auto (default) when the table exists; when it does not, a boot
30
+ # warning is logged, the security audit reports it, and changes
31
+ # still work — an upgrade is never broken by an unrun migration
32
+ # true always (a missing table then fails every change under the
33
+ # default :raise strategy)
34
+ # false never (events are still published as notifications)
35
+ attr_reader :audit_events
36
+
37
+ # Lambda(controller) returning who acts in an admin UI or API request: a
38
+ # user, an Audit::Actor, or a Hash { type:, id:, label: }. nil (default)
39
+ # falls back to current_user_resolver; under HTTP Basic auth the
40
+ # supplied username is recorded with actor_type 'basic_auth'.
41
+ # config.audit_actor_resolver = ->(controller) { controller.current_admin }
42
+ attr_accessor :audit_actor_resolver
43
+
44
+ # Lambda(controller) returning the request context stored with each
45
+ # event: a Hash with :request_id and :ip. nil (default) records the
46
+ # request id and the remote IP.
47
+ attr_accessor :audit_request_context
48
+
49
+ # What happens when a change event cannot be written (the table is
50
+ # unreachable, say) — change events are written inside the transaction
51
+ # of the change:
52
+ # :raise (default) the change rolls back: no change without its record
53
+ # :report the change goes through; the error is logged and handed to
54
+ # Rails.error (handled)
55
+ # :log the change goes through; the error is logged only
56
+ attr_reader :audit_change_failure_strategy
57
+
58
+ # What happens when a notification subscriber, a legacy hook, or the
59
+ # store of a runtime event fails: :report (default), :log or :raise.
60
+ attr_reader :audit_hook_failure_strategy
61
+
62
+ # Which runtime event groups are stored (every runtime event is published
63
+ # regardless). Default: %i[denied]. See RUNTIME_GROUPS.
64
+ attr_reader :audit_runtime_events
65
+
66
+ # When true, every audit event is also written to Rails.logger.
67
+ attr_reader :audit_log_subscriber
68
+
69
+ # How long events are kept: a duration (90.days), seconds, or nil
70
+ # (default) to keep them forever. Only the purge service deletes events,
71
+ # and only ones older than this.
72
+ attr_reader :audit_retention_period
73
+
74
+ # Lambda(controller) returning whether the request may read the audit
75
+ # log (pages, exports, the API endpoint). nil (default): anyone with
76
+ # admin access.
77
+ attr_accessor :audit_log_authorization
78
+
79
+ # Lambda(controller, target, user_ids, action) returning whether the
80
+ # request may change who holds a Permission Set or belongs to a group:
81
+ # target is the PermissionSet or PermissionSetGroup, user_ids the people
82
+ # concerned (cast to the user key type), action one of :grant, :revoke,
83
+ # :change_expiry, :review, :add_member, :remove_member. Asked by the HTML
84
+ # admin and by the API assignment endpoints (after
85
+ # api_assignment_authorization). A refusal changes nothing. nil
86
+ # (default): no extra check. See PermissionForge::AssignmentAuthorization.
87
+ attr_accessor :admin_assignment_authorization
88
+
89
+ # Sets this group's defaults; called from Configuration#initialize.
90
+ # @return [void]
91
+ # @api private
92
+ def set_audit_defaults
93
+ @audit_events = :auto
94
+ @audit_actor_resolver = nil
95
+ @audit_request_context = nil
96
+ @audit_change_failure_strategy = :raise
97
+ @audit_hook_failure_strategy = :report
98
+ @audit_runtime_events = %i[denied].freeze
99
+ @audit_log_subscriber = false
100
+ @audit_retention_period = nil
101
+ @audit_log_authorization = nil
102
+ @admin_assignment_authorization = nil
103
+ # a new configuration may become PermissionForge.configuration
104
+ audit_observation_changed!
105
+ end
106
+
107
+ # @raise [PermissionForge::ConfigurationError] for anything but :auto,
108
+ # true or false (a string from ENV would otherwise pass for :auto)
109
+ def audit_events=(value)
110
+ @audit_events = one_of!(:audit_events, value, AUDIT_EVENTS)
111
+ end
112
+
113
+ def audit_change_failure_strategy=(value)
114
+ @audit_change_failure_strategy = one_of!(:audit_change_failure_strategy, value, CHANGE_FAILURE_STRATEGIES)
115
+ end
116
+
117
+ def audit_hook_failure_strategy=(value)
118
+ @audit_hook_failure_strategy = one_of!(:audit_hook_failure_strategy, value, HOOK_FAILURE_STRATEGIES)
119
+ end
120
+
121
+ # Symbols or strings; nil means none.
122
+ def audit_runtime_events=(value)
123
+ groups = Array(value).map { |group| group.to_s.to_sym }
124
+ unknown = groups - RUNTIME_GROUPS
125
+ unless unknown.empty?
126
+ raise PermissionForge::ConfigurationError,
127
+ "audit_runtime_events: unknown group(s) #{unknown.map(&:inspect).join(', ')} " \
128
+ "(known: #{RUNTIME_GROUPS.map(&:inspect).join(', ')})"
129
+ end
130
+
131
+ # frozen: a group pushed in place would pass Audit::Gate by
132
+ @audit_runtime_events = groups.uniq.freeze
133
+ audit_observation_changed!
134
+ end
135
+
136
+ def audit_log_subscriber=(value)
137
+ @audit_log_subscriber = value
138
+ audit_observation_changed!
139
+ end
140
+
141
+ # @param group [Symbol, nil] a runtime group of the catalogue
142
+ # @return [Boolean] whether runtime events of that group are stored
143
+ def store_runtime_group?(group)
144
+ !group.nil? && audit_runtime_events.include?(group.to_sym)
145
+ end
146
+
147
+ # The one definition of a valid retention period (the setter and
148
+ # Services::AuditEventPurger both ask it).
149
+ #
150
+ # @param value [ActiveSupport::Duration, Numeric, nil]
151
+ # @return [ActiveSupport::Duration, nil] a Duration stays as given (calendar
152
+ # arithmetic), a number is seconds
153
+ # @raise [PermissionForge::ConfigurationError] for anything else
154
+ def self.retention_period(value)
155
+ return if value.nil?
156
+
157
+ period = value.is_a?(ActiveSupport::Duration) || !value.is_a?(Numeric) ? value : value.seconds
158
+ return period if period.is_a?(ActiveSupport::Duration) && period.to_i.positive?
159
+
160
+ raise PermissionForge::ConfigurationError,
161
+ "audit_retention_period must be a positive duration (90.days), a number of seconds or nil (got #{value.inspect})"
162
+ end
163
+
164
+ def audit_retention_period=(value)
165
+ AuditSettings.retention_period(value)
166
+ @audit_retention_period = value
167
+ end
168
+
169
+ private
170
+
171
+ # Audit::Gate keeps whether a runtime event is stored or heard.
172
+ def audit_observation_changed!
173
+ PermissionForge::Audit::Gate.changed! if defined?(PermissionForge::Audit::Gate)
174
+ end
175
+
176
+ def one_of!(option, value, allowed)
177
+ return value if allowed.include?(value)
178
+
179
+ raise PermissionForge::ConfigurationError,
180
+ "#{option} must be one of #{allowed.map(&:inspect).join(', ')} (got #{value.inspect})"
181
+ end
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ class Configuration
5
+ # Options for what happens when the cache store fails
6
+ # ({PermissionForge::Cache::Failure}) and for what is cached from inside a
7
+ # database transaction ({PermissionForge::OpenTransaction}).
8
+ #
9
+ # @api public
10
+ module CacheSettings
11
+ # Optional callback when the cache store fails an invalidation: a stamp
12
+ # or a removal it did not confirm after the retries, an entry that could
13
+ # not be removed, or an error inside an after_commit invalidation. The
14
+ # process that failed makes the invalidation when the store answers
15
+ # again (Cache::Backlog); until then, and in other processes, a cached
16
+ # permission hash may be out of date until cache_ttl.
17
+ #
18
+ # Nothing is raised (a raising after_commit makes Rails skip the
19
+ # callbacks of the remaining records); the failure is logged at error
20
+ # level, published as the `cache_failure.permission_forge` notification,
21
+ # and handed to this hook. Receives (operation:, user_id:, keys:,
22
+ # error:, **details). Errors raised by the hook are logged and swallowed.
23
+ #
24
+ # config.on_cache_failure = ->(operation:, user_id:, **) {
25
+ # Sentry.capture_message("permission cache: #{operation} failed for user #{user_id}")
26
+ # }
27
+ attr_accessor :on_cache_failure
28
+
29
+ # Whether a database transaction is treated as reading from a snapshot —
30
+ # the rows as they were when it began, whatever others have committed
31
+ # since (REPEATABLE READ or stronger). A permission hash built inside
32
+ # such a transaction can be older than an invalidation that has already
33
+ # finished, so it is not cached: the request uses it, and
34
+ # `regenerate_permissions` / `invalidate_permissions` called there take
35
+ # effect in the cache once the transaction has committed.
36
+ #
37
+ # :auto (default) decided from what Rails knows, without asking the
38
+ # server: yes on MySQL (its default isolation) and SQLite; on
39
+ # PostgreSQL only inside a transaction opened with
40
+ # `isolation: :repeatable_read` or `:serializable`. Any other
41
+ # adapter counts as a snapshot.
42
+ # true every transaction — for a PostgreSQL whose default isolation
43
+ # was raised for the database or the role
44
+ # (`default_transaction_isolation`).
45
+ # false none — for a MySQL run at READ COMMITTED. Inside a
46
+ # transaction everything is then cached and invalidated as it
47
+ # is outside one.
48
+ #
49
+ # Where transactions read from a snapshot, permissions first read
50
+ # inside a transaction are built once per transaction and not cached:
51
+ # read them once before a loop of saves.
52
+ attr_reader :snapshot_transactions
53
+
54
+ SNAPSHOT_TRANSACTIONS = [:auto, true, false].freeze
55
+
56
+ # @raise [PermissionForge::ConfigurationError] for anything else (a
57
+ # string from ENV, say), which would otherwise pass for :auto
58
+ def snapshot_transactions=(value)
59
+ unless SNAPSHOT_TRANSACTIONS.include?(value)
60
+ raise PermissionForge::ConfigurationError,
61
+ "snapshot_transactions must be :auto, true or false (got #{value.inspect})"
62
+ end
63
+
64
+ @snapshot_transactions = value
65
+ end
66
+
67
+ # Sets this group's defaults; called from Configuration#initialize.
68
+ # @return [void]
69
+ # @api private
70
+ def set_cache_failure_defaults
71
+ @on_cache_failure = nil
72
+ @snapshot_transactions = :auto
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ class Configuration
5
+ # Options for controller actions: discovery in the admin and the runtime
6
+ # guard ({PermissionForge::Concerns::ControllerActionGuard}).
7
+ #
8
+ # @api public
9
+ module ControllerActionSettings
10
+ # Rails' own controllers never show up in discovery or get guarded.
11
+ DEFAULT_CONTROLLER_DISCOVERY_EXCLUDE = [
12
+ %r{\A(rails|active_storage|action_mailbox|action_cable|action_text|turbo)(/|\z)}
13
+ ].freeze
14
+
15
+ # When true, a Resource's Actions tab lists the host app's controller
16
+ # actions that map to that resource. Default: false.
17
+ attr_accessor :controller_action_discovery
18
+
19
+ # Maps a controller action to a Resource and an action key. Receives
20
+ # (controller_path, action_name) — e.g. ('customers/merges', 'create').
21
+ #
22
+ # ['Customer', 'merge'] — this Resource, this action key
23
+ # false — never managed (not listed, not guarded)
24
+ # nil — use the convention (controller name → model)
25
+ #
26
+ # config.controller_action_resolver = lambda do |controller_path, action_name|
27
+ # ['Customer', 'merge'] if controller_path == 'customers/merges' && action_name == 'create'
28
+ # end
29
+ attr_accessor :controller_action_resolver
30
+
31
+ # Controllers left out of discovery and the guard: Regexps, or Strings
32
+ # naming a controller or a namespace ('webhooks' leaves out `webhooks`
33
+ # and `webhooks/*`, not `webhooks_admin`). Blank entries are ignored.
34
+ # PermissionForge's own controllers are always excluded.
35
+ attr_accessor :controller_discovery_exclude
36
+
37
+ # What the guard does with a resource or action no Permission Set manages:
38
+ #
39
+ # :allow — let it through (default — nothing is restricted until an admin
40
+ # adds the action to a Permission Set)
41
+ # :block — deny until a Permission Set permits it
42
+ #
43
+ # A String is taken as its Symbol; any other value raises
44
+ # ConfigurationError (StrategyValues).
45
+ attr_accessor :unconfigured_action_strategy
46
+
47
+ # Lambda(user, controller) returning true to skip the guard for this
48
+ # request (e.g. ->(user, _controller) { user.admin? }). Default: nil.
49
+ attr_accessor :action_guard_bypass
50
+
51
+ # What the guard does with a mapped action when there is no current
52
+ # user, apart from nil_user_strategy (which keeps governing record
53
+ # rules and masking):
54
+ #
55
+ # nil — follow nil_user_strategy (default: no change for existing apps)
56
+ # :bypass — let the request through
57
+ # :deny — refuse it (PermissionForge::ActionNotAuthorizedError)
58
+ #
59
+ # :raise refuses too; nil or blank follows nil_user_strategy; a String
60
+ # is taken as its Symbol; any other value raises ConfigurationError
61
+ # (StrategyValues). New installs get :deny in the
62
+ # generated initializer.
63
+ attr_accessor :action_guard_nil_user_strategy
64
+
65
+ # Sets this group's defaults; called from Configuration#initialize.
66
+ # @return [void]
67
+ # @api private
68
+ def set_controller_action_defaults
69
+ @controller_action_discovery = false
70
+ @controller_action_resolver = nil
71
+ @controller_discovery_exclude = DEFAULT_CONTROLLER_DISCOVERY_EXCLUDE.dup
72
+ @unconfigured_action_strategy = :allow
73
+ @action_guard_bypass = nil
74
+ @action_guard_nil_user_strategy = nil
75
+ end
76
+
77
+ # @return [Boolean] whether the guard lets a request with no user
78
+ # through: action_guard_nil_user_strategy, else nil_user_strategy.
79
+ # Only the exact :bypass value lets it through.
80
+ def action_guard_nil_user_bypass?
81
+ (action_guard_nil_user_strategy.presence || nil_user_strategy).to_s == 'bypass'
82
+ end
83
+
84
+ # @return [Boolean] whether the admin lists the app's controller actions.
85
+ def controller_action_discovery?
86
+ controller_action_discovery == true
87
+ end
88
+
89
+ # @return [Boolean] whether an unmanaged resource or action passes the
90
+ # guard. Only the exact :allow value allows.
91
+ def allow_unconfigured_actions?
92
+ unconfigured_action_strategy.to_s == 'allow'
93
+ end
94
+
95
+ # @param controller_path [String] e.g. 'api/v1/customers'
96
+ # @return [Boolean] whether the controller is outside discovery and the guard.
97
+ def controller_excluded?(controller_path)
98
+ path = controller_path.to_s
99
+ return true if path == 'permission_forge' || path.start_with?('permission_forge/')
100
+
101
+ Array(controller_discovery_exclude).any? do |pattern|
102
+ next pattern.match?(path) if pattern.is_a?(Regexp)
103
+
104
+ # A blank String (an unset ENV value) must not exclude everything,
105
+ # and 'custom' must not exclude 'customers'.
106
+ prefix = pattern.to_s.chomp('/')
107
+ prefix.present? && (path == prefix || path.start_with?("#{prefix}/"))
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ class Configuration
5
+ # GDPR-related configuration methods extracted for SRP.
6
+ #
7
+ # @api public
8
+ module GdprSettings
9
+ # Turns the GDPR masking defaults on or off (an explicit setting wins over a preset).
10
+ # @param value [Boolean]
11
+ def gdpr_defaults=(value)
12
+ @gdpr_defaults_explicitly_set = true
13
+ @gdpr_defaults = value
14
+ end
15
+
16
+ # Replaces the patterns that mark a field name as personal data.
17
+ # @param patterns [Array<Regexp>]
18
+ # @raise [ConfigurationError] for anything that is not a list of regular expressions
19
+ def gdpr_field_patterns=(patterns)
20
+ validate_gdpr_patterns!(patterns)
21
+ @gdpr_field_patterns = patterns
22
+ end
23
+
24
+ def gdpr_defaults_explicitly_set?
25
+ @gdpr_defaults_explicitly_set
26
+ end
27
+
28
+ # Remembers the digest of the current patterns (it tells a changed list from the default).
29
+ # @return [String]
30
+ # @api private
31
+ def record_gdpr_pattern_checksum
32
+ @gdpr_pattern_checksum = compute_pattern_checksum
33
+ end
34
+
35
+ def gdpr_patterns_changed_since_generation?
36
+ return false unless @gdpr_pattern_checksum
37
+
38
+ @gdpr_pattern_checksum != compute_pattern_checksum
39
+ end
40
+
41
+ def validate_gdpr_explicit_choice!
42
+ return unless @gdpr_require_explicit_choice
43
+ return if gdpr_defaults_explicitly_set?
44
+
45
+ raise PermissionForge::ConfigurationError,
46
+ 'gdpr_require_explicit_choice is enabled but gdpr_defaults was not explicitly set. ' \
47
+ 'Please set config.gdpr_defaults = true or config.gdpr_defaults = false in your initializer.'
48
+ end
49
+
50
+ private
51
+
52
+ def validate_gdpr_patterns!(patterns)
53
+ return if patterns.blank?
54
+
55
+ patterns.each_key do |key|
56
+ next if key.is_a?(Regexp)
57
+
58
+ Regexp.new(key)
59
+ rescue RegexpError => e
60
+ raise PermissionForge::ConfigurationError,
61
+ "Invalid GDPR field pattern for '#{key}': #{e.message}"
62
+ end
63
+ end
64
+
65
+ def compute_pattern_checksum
66
+ Digest::SHA256.hexdigest(@gdpr_field_patterns.to_a.map { |k, v| "#{k.source}:#{v}" }.sort.join('|'))
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ class Configuration
5
+ # Configuration for {PermissionForge::Concerns::MaskedAttributes} model-level enforcement.
6
+ #
7
+ # @api public
8
+ module MaskedAttributesSettings
9
+ # Value returned by MaskedAttributes when read_access is denied. Default: nil.
10
+ attr_accessor :hidden_attribute_value
11
+
12
+ # When true (default), MaskedAttributes omits hidden keys from `attributes` /
13
+ # `as_json` / `serializable_hash`. When false, keys are present with hidden_attribute_value.
14
+ attr_accessor :omit_hidden_in_attributes_hash
15
+
16
+ # Controls auto-applying MaskedAttributes to host models.
17
+ #
18
+ # :auto / true — apply to every model that has a Resource record (default)
19
+ # ['Customer', 'Order'] — only apply to these named models
20
+ # false / nil — no auto-apply; dev includes the concern manually
21
+ #
22
+ # Auto-apply runs at engine boot (scanning existing Resource rows) and again
23
+ # whenever a new Resource is created at runtime. Both paths respect this setting.
24
+ attr_accessor :auto_apply_masked_attributes
25
+
26
+ # Protects against persisting a masked/hidden value back to the database.
27
+ # MaskedAttributes overrides column getters, so a host callback like
28
+ # `before_save { self.email = email.strip }` would otherwise read the MASKED
29
+ # value and write it back, corrupting the stored data. This guard remembers
30
+ # the exact sentinel handed out for each attribute and, when that same
31
+ # sentinel is written back, prevents the overwrite.
32
+ #
33
+ # :skip — silently keep the original stored value (default, safe)
34
+ # :raise — raise MaskedAttributes::MaskedWriteError (surfaces the bug)
35
+ # :off — disable the guard (legacy behavior; risks silent corruption)
36
+ attr_accessor :masked_write_protection
37
+
38
+ # What the attribute guard does when there is no current user (a job, a
39
+ # mailer, the console, a resolver that returns nil). Nothing else
40
+ # governs masking without a user: nil_user_strategy is about rows,
41
+ # response_filter_nil_user_strategy about FiltersResponseAttributes.
42
+ #
43
+ # :bypass — every getter returns the stored value (default)
44
+ # :hide — every guarded attribute reads as hidden_attribute_value:
45
+ # getters, read_attribute, `[]`, attributes, as_json and
46
+ # the wrapped virtual readers and store accessors. The
47
+ # primary key and the raw paths (read_attribute_before_type_cast,
48
+ # pluck) are untouched, as for a signed-in user. Code that
49
+ # reads masked models with no user then sees hidden values:
50
+ # set the current user in those jobs and mailers. The user
51
+ # model (user_class and its subclasses) is exempt: sign-in
52
+ # reads it before anyone is signed in (encrypted_password,
53
+ # confirmation_token, lockable counters).
54
+ attr_accessor :masked_attributes_nil_user_strategy
55
+
56
+ # @return [Boolean] whether the strategy is :hide (whatever the model)
57
+ def hide_masked_attributes_without_user?
58
+ @masked_attributes_nil_user_strategy.to_s == 'hide'
59
+ end
60
+
61
+ # The one decision of whether a record of klass hides its guarded
62
+ # attributes when nobody is signed in: under :hide, every model but the
63
+ # user model.
64
+ #
65
+ # @param klass [Class]
66
+ # @return [Boolean]
67
+ def hide_masked_attributes_without_user_for?(klass)
68
+ hide_masked_attributes_without_user? && !user_model_class?(klass)
69
+ end
70
+
71
+ # Sets this group's defaults; called from Configuration#initialize.
72
+ # @return [void]
73
+ # @api private
74
+ def set_masked_attributes_defaults
75
+ @hidden_attribute_value = nil
76
+ @omit_hidden_in_attributes_hash = true
77
+ @auto_apply_masked_attributes = :auto
78
+ @masked_write_protection = :skip
79
+ @masked_attributes_nil_user_strategy = :bypass
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ class Configuration
5
+ # Options for the write path: {PermissionForge::Services::PermitsAttributes},
6
+ # which filters incoming params by field-level edit access.
7
+ #
8
+ # @api public
9
+ module PermitsAttributesSettings
10
+ # How PermitsAttributes treats a `<assoc>_attributes` payload when the
11
+ # user's permission hash has no entry for the association's model:
12
+ # false — the payload is dropped (default, fail closed)
13
+ # true — it is filtered against the PARENT resource's fields, as
14
+ # versions before 0.14 did (a key editable on the parent then
15
+ # admits the same key on a child the user was never granted)
16
+ attr_accessor :nested_attributes_parent_fallback
17
+
18
+ # Sets this group's defaults; called from Configuration#initialize.
19
+ # @return [void]
20
+ # @api private
21
+ def set_permits_attributes_defaults
22
+ @nested_attributes_parent_fallback = false
23
+ end
24
+ end
25
+ end
26
+ end