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,624 @@
1
+ module PermissionForge
2
+ # Raised for an invalid or missing setting, or an integration the host did not wire.
3
+ #
4
+ # @api public
5
+ class ConfigurationError < StandardError; end
6
+
7
+ # Every setting of the gem, set in a `PermissionForge.configure` block; the groups of options live in
8
+ # `Configuration::*Settings` modules. Defaults are the library defaults; `config.preset` closes or restores them in one line.
9
+ #
10
+ # @api public
11
+ class Configuration # rubocop:disable Metrics/ClassLength
12
+ # User model class name (string, lazily constantized)
13
+ attr_accessor :user_class
14
+
15
+ # Lambda that returns the current user or nil
16
+ attr_accessor :current_user_resolver
17
+
18
+ # Lambda that checks if a user is active (receives user, returns boolean).
19
+ # Read by the security audit (assignments held by inactive users); it does
20
+ # not remove access by itself.
21
+ attr_accessor :user_active_check
22
+
23
+ # Lambda that returns an enumerable/AR relation of users eligible for
24
+ # admin UI assignment dropdowns.
25
+ #
26
+ # Default: `user_model.order(:id)` — chosen because `:name` is often a
27
+ # virtual attribute or a Ruby method (e.g., `"#{first_name} #{last_name}"`)
28
+ # rather than a real column, so a SQL ORDER BY would fail.
29
+ #
30
+ # The User class must respond to `:name` and `:email` (as method or column)
31
+ # for display labels. Override to scope or sort however you like:
32
+ # config.user_list_resolver = -> { User.active.order(:last_name, :first_name) }
33
+ attr_accessor :user_list_resolver
34
+
35
+ # Hard cap on how many users the admin UI assignment dropdowns render.
36
+ # Applied on top of user_list_resolver (after excluding already-assigned
37
+ # users), so an unscoped host users table can never turn a show page into
38
+ # a six-figure <option> list. When the cap kicks in, the dropdown shows a
39
+ # localized "first N users" hint prompting a narrower user_list_resolver.
40
+ # A resolver relation carrying its own smaller LIMIT is preserved.
41
+ # Default: 500. Set to nil to disable (not recommended for large tables).
42
+ attr_accessor :user_list_limit
43
+
44
+ # Lambda that formats a user for the assignment dropdown label.
45
+ # Receives the user, returns a String (the human-readable part).
46
+ # The combobox automatically appends ` #<id>` so the JS can extract the id.
47
+ #
48
+ # Default: tries name, full_name, display_name (in that order), then email,
49
+ # then a localized fallback. Override for full control:
50
+ # config.user_label_formatter = ->(u) { "#{u.last_name}, #{u.first_name} <#{u.email}>" }
51
+ attr_accessor :user_label_formatter
52
+
53
+ # Cache backend:
54
+ # :rails_cache — uses Rails.cache (works with any Rails cache store:
55
+ # redis_cache_store, solid_cache_store, mem_cache_store,
56
+ # file_store, memory_store, etc.)
57
+ # :memory — simple in-memory hash (good for dev/test, no dependencies)
58
+ # :null — no caching (permissions recomputed every time)
59
+ # ActiveSupport::Cache::Store instance — use a specific cache store directly
60
+ attr_accessor :cache_backend
61
+
62
+ # Key prefix for cache entries (avoids collisions with other cached data)
63
+ attr_accessor :cache_key_prefix
64
+
65
+ # Cache TTL in seconds for permission entries. Cached permissions expire
66
+ # after this duration as a staleness backstop — the cache is rebuilt
67
+ # transparently on the next read (read-through), so a TTL never leaves a
68
+ # user without permissions.
69
+ # Default: 3600 (1 hour). Set to nil for no expiration (not recommended).
70
+ attr_accessor :cache_ttl
71
+
72
+ # Cache TTL in seconds applied when any of the user's permission sets has
73
+ # activation conditions (session flag, IP allowlist, login hours). The
74
+ # conditions are evaluated on every read, against the request that reads
75
+ # (ConditionalSets); this clamps the base entry and its per-context
76
+ # variants as a backstop. The effective TTL is
77
+ # min(cache_ttl, conditional_set_cache_ttl).
78
+ # Default: 300 (5 minutes). Set to nil to disable the clamp.
79
+ attr_accessor :conditional_set_cache_ttl
80
+
81
+ # Table name prefix for all permission_forge tables
82
+ attr_accessor :table_name_prefix
83
+
84
+ # Lambda that generates UUIDs
85
+ attr_accessor :uuid_generator
86
+
87
+ # Enable created_by_id/updated_by_id tracking
88
+ attr_accessor :audit_trail_enabled
89
+
90
+ # Class name for audit trail belongs_to associations
91
+ attr_accessor :audit_user_class
92
+
93
+ # Fields to ignore when auto-generating resource fields
94
+ attr_accessor :ignored_fields
95
+
96
+ # Hash of regex => lambda for field name variants
97
+ # e.g. /_id\z/ => ->(name) { name.sub(/_id\z/, '_uuid') }
98
+ attr_accessor :field_variant_mappings
99
+
100
+ # Method name that models can define for extra permission fields
101
+ attr_accessor :extra_fields_method
102
+
103
+ # Lambda that builds the profile hash cached alongside permissions
104
+ # Receives user, returns hash
105
+ attr_accessor :profile_builder
106
+
107
+ # Lambda that returns the current request context for activation conditions.
108
+ # Returns a hash with optional keys: :ip, :session_flags, :time
109
+ # Default: -> { {} } (no conditions — backward compatible)
110
+ # Example: -> { { ip: Current.ip, session_flags: Current.session_flags, time: Time.current } }
111
+ # Called on every permission read of a user who holds a conditional set:
112
+ # keep it cheap, return {} (or anything with #to_h) where there is no
113
+ # request, and never read permissions inside it (that raises
114
+ # ConfigurationError).
115
+ attr_accessor :request_context_resolver
116
+
117
+ # How activation-condition sets (session flag / allowed_ips / login hours) are
118
+ # treated when request_context_resolver returns a blank context.
119
+ # false (default) — treat conditional sets as active (backward compatible; a
120
+ # boot warning fires so the fail-open is visible)
121
+ # true — fail closed: with no request context (nothing, or neither
122
+ # an IP nor session flags — a job, the console) conditional
123
+ # GRANTING sets are dropped and conditional MUTING sets
124
+ # still apply; with a context, a MUTING set whose
125
+ # condition cannot be judged (no IP for allowed_ips, no
126
+ # session_flags key for a flag) applies
127
+ # Recommended: set to true.
128
+ attr_accessor :conditional_sets_strict_mode
129
+
130
+ # Multi-tenancy via acts_as_tenant (optional, nil = disabled).
131
+ # Set to the tenant model class name (e.g., 'Account') to enable.
132
+ # Requires the acts_as_tenant gem and a tenant column on all PF tables.
133
+ # Boot raises ConfigurationError when this is set but the gem is not
134
+ # loaded, so a misconfigured install can never run without isolation.
135
+ # Rows whose tenant column is NULL are only visible while no tenant is
136
+ # set (console, jobs); acts_as_tenant hides them under every tenant.
137
+ attr_accessor :tenant_model
138
+
139
+ # Foreign key column name for tenant association.
140
+ # Default: inferred from tenant_model (e.g., 'Account' -> :account_id,
141
+ # 'Billing::Account' -> :account_id)
142
+ attr_accessor :tenant_column
143
+
144
+ # Admin UI — HTTP Basic Auth (enabled by default, like Mission Control)
145
+ # If enabled but credentials not set, returns 401 Unauthorized.
146
+ # Set credentials via config or Rails credentials.
147
+ attr_accessor :http_basic_auth_enabled
148
+ # The admin UI's HTTP Basic username and password (see http_basic_auth_enabled).
149
+ # @return [String, nil]
150
+ attr_accessor :http_basic_auth_user, :http_basic_auth_password
151
+
152
+ # Base controller class for admin UI (default: "::ActionController::Base")
153
+ # Override to inherit from your own admin controller (e.g. "::AdminController")
154
+ # which handles authentication via Devise, Warden, etc.
155
+ attr_accessor :base_controller_class
156
+
157
+ # JSON Admin API (disabled by default — opt-in)
158
+ # When enabled, mounts RESTful JSON endpoints under /api/ within the engine.
159
+ attr_accessor :api_enabled
160
+
161
+ # Lambda that receives the controller instance and returns true/false for auth.
162
+ # When nil and API enabled, all requests return 401 (safe default).
163
+ # Example: ->(controller) { controller.current_user&.has_role?(:admin) }
164
+ attr_accessor :api_auth
165
+
166
+ # Base controller class for API controllers (default: "::ActionController::Base").
167
+ # Override to inherit from your own base (e.g. "::Api::V1::Admin::BaseController").
168
+ attr_accessor :api_base_controller_class
169
+
170
+ # Lambda that paginates a scope: receives (scope, page, per_page) -> paginated scope.
171
+ # When nil, a built-in LIMIT/OFFSET paginator is used.
172
+ # Must respond to .total_count for meta. Example: ->(s, p, pp) { s.page(p).per(pp) }
173
+ attr_accessor :api_pagination_handler
174
+
175
+ # Maximum records per page for API collection endpoints.
176
+ # Hard cap applied regardless of client-requested per_page.
177
+ attr_accessor :api_max_per_page
178
+
179
+ # Default records per page when per_page param is not provided.
180
+ attr_accessor :api_default_per_page
181
+
182
+ # When true, the /api/stats endpoint requires admin access.
183
+ # The api_auth lambda must set @admin = true on the controller.
184
+ attr_accessor :api_stats_require_admin
185
+
186
+ # Record identifier field for API lookups (default: :uuid).
187
+ # Change to :id if your app uses integer IDs in URLs.
188
+ attr_accessor :api_record_identifier
189
+
190
+ # Hash of serializer overrides for API responses.
191
+ # Keys are resource symbols, values are serializer classes.
192
+ # Built-in serializers: respond to .new(record).as_json and .collection(records).
193
+ # AMS serializers: also work — the base controller detects and adapts automatically.
194
+ # Example: { permission_set: MyApp::PermissionSetSerializer }
195
+ attr_accessor :api_serializer_overrides
196
+
197
+ # When true, PermissionSetRuleSerializer replaces field_value with "[REDACTED]"
198
+ # in API responses. Rule field_values can contain sensitive data (user IDs,
199
+ # account numbers, business logic values). Enable for GDPR/HIPAA environments.
200
+ # Default: false (backward-compatible)
201
+ attr_accessor :api_redact_rule_field_values
202
+
203
+ # Strategy when PermissionForge.current_user is nil in PermissionScopedFindable:
204
+ # :bypass — silent bypass, returns unscoped results (backward-compatible default)
205
+ # :raise — raises NotAuthorizedError
206
+ # :deny — returns nil (record not found)
207
+ attr_accessor :nil_user_strategy
208
+
209
+ # Lambda for action-level authorization on API mutation endpoints (clone, etc.).
210
+ # Receives (controller, action_name, record) and should return true/false.
211
+ # When nil, no extra authorization is performed (backward-compatible).
212
+ # Example: ->(controller, action, record) { controller.current_user&.admin? }
213
+ attr_accessor :api_action_authorization
214
+
215
+ # Lambda for authorization checks on user-to-permission-set assignments
216
+ # and group memberships. Receives (controller, target, user_ids): the
217
+ # Permission Set (or Permission Set Group), and an Array of the people
218
+ # concerned cast to the user model's primary-key type (Integers for
219
+ # integer keys) on every endpoint. Returns true/false. When nil, the
220
+ # request is refused while api_require_authorization is on (the default).
221
+ attr_accessor :api_assignment_authorization
222
+
223
+ # When true, API mutation and assignment endpoints REQUIRE an authorization
224
+ # lambda to be configured: if api_action_authorization / api_assignment_authorization
225
+ # is nil, the request is denied (403) instead of silently allowed. This closes
226
+ # the "api_auth proves identity but not admin-ness" escalation, where any
227
+ # authenticated caller could otherwise self-grant a permission set.
228
+ # true (default) — nil authorizer = deny
229
+ # false — nil authorizer = allow: every caller api_auth lets in
230
+ # may change permissions (opt-out; boot warning fires)
231
+ attr_accessor :api_require_authorization
232
+
233
+ # Optional callback when view_all/modify_all bypasses row-level rules.
234
+ # Receives (user:, resource:, reason:). Errors are swallowed to prevent
235
+ # callback failures from breaking queries.
236
+ attr_reader :on_rule_bypass
237
+
238
+ # I18n — available locales for the admin UI.
239
+ # When set, only matching locale files are loaded (performance optimization).
240
+ # When nil (default), falls back to PermissionForge::BUNDLED_LOCALES — the
241
+ # locales the gem ships translations for ([:en, :sq, :mk]). Avoids the
242
+ # admin UI rendering host-app locales (e.g. Faker) the gem can't translate.
243
+ # Example: [:en, :sq, :mk]
244
+ attr_accessor :available_locales
245
+
246
+ # Optional callback for field-level audit logging.
247
+ # Called for each field processed by FiltersResponseAttributes.
248
+ # Receives: lambda(resource_name:, field_name:, action:, user:)
249
+ # action is one of: :hidden, :masked, :passed
250
+ attr_reader :on_field_filtered
251
+
252
+ # Both hooks are subscribers of the runtime audit events
253
+ # (Audit::Subscribers::LegacyHooks): setting one tells Audit::Gate.
254
+ def on_rule_bypass=(hook)
255
+ @on_rule_bypass = hook
256
+ PermissionForge::Audit::Gate.changed! if defined?(PermissionForge::Audit::Gate)
257
+ end
258
+
259
+ def on_field_filtered=(hook)
260
+ @on_field_filtered = hook
261
+ PermissionForge::Audit::Gate.changed! if defined?(PermissionForge::Audit::Gate)
262
+ end
263
+
264
+ # Strategy for fields not mapped in the permission hash:
265
+ # :block — exclude unmapped fields from response (secure default)
266
+ # :pass_through — field passes through unmasked (use only if all fields are explicitly mapped)
267
+ # :warn — pass through but log a warning
268
+ # :mask_all — apply full masking to unconfigured fields
269
+ # Unknown values fall back to :block (fail closed).
270
+ attr_accessor :unconfigured_field_strategy
271
+
272
+ # How FiltersResponseAttributes behaves when there is no current user
273
+ # (system/console/background context):
274
+ # :bypass — return attributes unfiltered (default, backward-compatible)
275
+ # :block — return {} (strict fail-closed)
276
+ # When a user IS present but has no configured access to the resource,
277
+ # every field is routed through unconfigured_field_strategy regardless of
278
+ # this setting (fail closed).
279
+ attr_accessor :response_filter_nil_user_strategy
280
+
281
+ # Custom mask strategies: Hash of strategy_name => callable(value)
282
+ # Merged with built-in strategies in MaskValue.strategies
283
+ attr_accessor :custom_mask_strategies
284
+
285
+ # GDPR defaults: when true, ResourceFieldGenerator auto-masks fields
286
+ # matching known GDPR-sensitive patterns (email, phone, name, address, etc.)
287
+ attr_reader :gdpr_defaults
288
+
289
+ # When true, raises ConfigurationError at boot if gdpr_defaults was never explicitly set.
290
+ attr_accessor :gdpr_require_explicit_choice
291
+
292
+ # When true, automatically appends GDPR field patterns to
293
+ # Rails.application.config.filter_parameters during engine initialization.
294
+ # This ensures sensitive fields (email, phone, SSN, etc.) are redacted in
295
+ # Rails logs, error reporters, and parameter inspect output.
296
+ # Default: true (enabled by default for security)
297
+ attr_accessor :filter_parameters_integration
298
+
299
+ # Hash of regex => strategy string for GDPR field pattern matching
300
+ attr_reader :gdpr_field_patterns
301
+
302
+ DEFAULT_IGNORED_FIELDS = %w[
303
+ id created_at updated_at token_digest token_last_digits
304
+ created_by updated_by token password encrypted_password
305
+ deleted_at uuid deactivated
306
+ ].freeze
307
+
308
+ DEFAULT_FIELD_VARIANT_MAPPINGS = {
309
+ /_id\z/ => ->(name) { "#{name.sub(/_id\z/, '')}_uuid" },
310
+ /_cents\z/ => ->(name) { name.sub(/_cents\z/, '') }
311
+ }.freeze
312
+
313
+ # Column names that are personal data, with the mask for each. One
314
+ # definition for masking detection (ResourceFieldGenerator,
315
+ # GdprFieldAuditor, Admin::SensitiveColumns, the security audit) and for
316
+ # Rails' filter_parameters (PermissionForge.gdpr_filter_parameters), which
317
+ # Rails also applies to ActiveRecord::Base.filter_attributes — so a match
318
+ # prints as [FILTERED] in #inspect. A word matches anywhere in the name,
319
+ # as Rails' own filter_parameter_logging does (`phone` in `cellphone`); a
320
+ # stem short enough to sit inside an unrelated word (tel, city, zip, dob,
321
+ # fax, mobile) matches a whole name segment (between `_`, the ends, a
322
+ # digit or a camelCase capital) or the start of one: `telephone`,
323
+ # `home_tel`, `tel2`, `telNumber`, `cityName`, `zipcode` yes — `hotel_id`,
324
+ # `capacity`, `telemetry_enabled`, `automobile_id`, `gzip_enabled` no.
325
+ # Every alternative stands alone (no group with an inner `|`):
326
+ # Admin::SensitiveColumns names the one that matched.
327
+ DEFAULT_GDPR_FIELD_PATTERNS = {
328
+ /\Aemail\z|_email\z/ => 'email',
329
+ /phone|telefon|\Amobile|_mobile|\Afax|_fax|\Atel\z|\Atel_|\Atel[A-Z0-9]|_tel\z|_tel_|_tel[A-Z0-9]/ => 'last4',
330
+ /\Assn\z|social_security|national_id/ => 'last4',
331
+ /iban|bank_account|credit_card|card_number/ => 'last4',
332
+ /\Aname\z|first_name|last_name|full_name|contact_name/ => 'first1_last4',
333
+ /address|street|\Acity\z|\Acity_|\Acity[A-Z0-9]|_city\z|_city_|_city[A-Z0-9]|\Azip|_zip|postal/ => 'full',
334
+ /date_of_birth|\Adob\z|\Adob_|\Adob[A-Z0-9]|_dob\z|_dob_|_dob[A-Z0-9]|birthday/ => 'full',
335
+ /ip_address|\Aip\z/ => 'full',
336
+ # Matched by accident before the short stems became whole segments
337
+ # (`ethnicity` through `city`, `telegram_*` through `tel`); named here.
338
+ /ethnic|telegram/ => 'full'
339
+ }.freeze
340
+
341
+ include GdprSettings
342
+ include MaskedAttributesSettings
343
+ include PermitsAttributesSettings
344
+ include QueryScopingSettings
345
+ include AdminUiSettings
346
+ include VirtualAttributesSettings
347
+ include ControllerActionSettings
348
+ include CacheSettings
349
+ include AuditSettings
350
+ prepend StrategyValues
351
+ # In front of StrategyValues: an option counts as set only once its value passed.
352
+ prepend Presets
353
+
354
+ def initialize
355
+ @gdpr_defaults_explicitly_set = false
356
+ set_preset_defaults
357
+ set_core_defaults
358
+ set_admin_defaults
359
+ set_masked_attributes_defaults
360
+ set_permits_attributes_defaults
361
+ set_query_scoping_defaults
362
+ set_admin_ui_defaults
363
+ set_virtual_attributes_defaults
364
+ set_controller_action_defaults
365
+ set_cache_failure_defaults
366
+ set_audit_defaults
367
+ end
368
+
369
+ # @return [Class] the class of `config.user_class`
370
+ def user_model
371
+ @user_class.constantize
372
+ end
373
+
374
+ # Whether klass is the user model (user_class) or one of its subclasses.
375
+ # False when user_class names no class. The answer is kept per user_class
376
+ # value and per class (by identity): a class reloaded in development is a
377
+ # new object and is asked again; so is every class after user_class
378
+ # changes.
379
+ #
380
+ # @param klass [Class, nil]
381
+ # @return [Boolean]
382
+ def user_model_class?(klass)
383
+ return false unless klass.is_a?(Class)
384
+
385
+ answers = (@user_model_answers ||= {})[@user_class] ||= {}.compare_by_identity
386
+ answers.fetch(klass) do
387
+ model = @user_class.to_s.safe_constantize
388
+ answers[klass] = model.is_a?(Class) && klass <= model
389
+ end
390
+ end
391
+
392
+ # Field-name variants produced by the configured mappings (e.g. `user_id`
393
+ # -> `user_uuid`, `price_cents` -> `price`). Shared by the granting and
394
+ # muting phases of PermissionHashBuilder so they always cover the same
395
+ # names.
396
+ #
397
+ # @param field_name [String, Symbol]
398
+ # @return [Array<String>]
399
+ def field_variants_for(field_name)
400
+ name = field_name.to_s
401
+ field_variant_mappings.filter_map do |pattern, transform|
402
+ transform.call(name) if name.match?(pattern)
403
+ end
404
+ end
405
+
406
+ # Returns an enumerable/AR relation of users for admin UI dropdowns.
407
+ # Uses `user_list_resolver` when configured, otherwise falls back to
408
+ # `user_model.order(:id)` — schema-agnostic because `:name` is frequently
409
+ # a method or virtual attribute, not a real column.
410
+ def user_list
411
+ if @user_list_resolver
412
+ @user_list_resolver.call
413
+ else
414
+ user_model.order(:id)
415
+ end
416
+ end
417
+
418
+ # Convenience: returns user_list with the given ids excluded, capped at
419
+ # user_list_limit. Works for both AR relations (where.not/limit) and plain
420
+ # arrays (reject/first). Exclusion happens before the cap so assigned
421
+ # users never consume dropdown slots.
422
+ def assignable_users(exclude_ids: [])
423
+ scope = user_list
424
+ ids = Array(exclude_ids).compact
425
+ if ids.any?
426
+ scope = if scope.respond_to?(:where)
427
+ scope.where.not(id: ids)
428
+ else
429
+ scope.reject { |u| ids.include?(u.id) }
430
+ end
431
+ end
432
+ apply_user_list_limit(scope)
433
+ end
434
+
435
+ # @return [Class] the class of `config.audit_user_class` (who created or edited a row)
436
+ def audit_user_model
437
+ @audit_user_class.constantize
438
+ end
439
+
440
+ # @return [Cache::Base] the permission cache, built from the cache settings on first use
441
+ def cache_store
442
+ @cache_store ||= build_cache_store
443
+ end
444
+
445
+ # Drops the built cache store (the next read builds a new one) and retires every `permissions_hash` memo.
446
+ # @return [void]
447
+ def reset_cache_store!
448
+ @cache_store = nil
449
+ # A new store invalidates every instance-level permissions_hash memo:
450
+ # memoized values were read from the store being discarded.
451
+ PermissionForge.bump_permissions_cache_version!
452
+ # ... and no invalidation is owed to it.
453
+ PermissionForge::Cache::Invalidation.reset!
454
+ end
455
+
456
+ private
457
+
458
+ # Caps the dropdown user list at user_list_limit.
459
+ def apply_user_list_limit(scope)
460
+ limit = user_list_limit
461
+ return scope unless limit&.positive?
462
+ return cap_relation_limit(scope, limit) if scope.respond_to?(:limit)
463
+ return scope.first(limit) if scope.respond_to?(:first)
464
+
465
+ scope
466
+ end
467
+
468
+ # An AR relation already carrying a smaller LIMIT (from user_list_resolver)
469
+ # is left untouched — relation.limit(n) would silently WIDEN it.
470
+ def cap_relation_limit(scope, limit)
471
+ existing = scope.respond_to?(:limit_value) ? scope.limit_value : nil
472
+ return scope if existing && existing <= limit
473
+
474
+ scope.limit(limit)
475
+ end
476
+
477
+ def set_core_defaults
478
+ set_user_defaults
479
+ set_cache_defaults
480
+ set_field_defaults
481
+ end
482
+
483
+ def set_user_defaults
484
+ @user_class = 'User'
485
+ @current_user_resolver = default_current_user_resolver
486
+ @user_active_check = default_user_active_check
487
+ @user_list_resolver = nil
488
+ @user_list_limit = 500
489
+ @user_label_formatter = nil
490
+ @audit_trail_enabled = true
491
+ @audit_user_class = 'User'
492
+ @profile_builder = default_profile_builder
493
+ @request_context_resolver = -> { {} }
494
+ @conditional_sets_strict_mode = false
495
+ @tenant_model = nil
496
+ @tenant_column = nil
497
+ end
498
+
499
+ public
500
+
501
+ def tenant_enabled?
502
+ @tenant_model.present?
503
+ end
504
+
505
+ # Association acts_as_tenant declares on every permission model (e.g.
506
+ # :account). Namespaces are dropped: `belongs_to :"billing/account"` is not
507
+ # a valid association, so 'Billing::Account' also yields :account.
508
+ def tenant_association_name
509
+ return nil unless tenant_enabled?
510
+
511
+ tenant_base_name.to_sym
512
+ end
513
+
514
+ # Foreign-key column acts_as_tenant filters on. Honours tenant_column,
515
+ # otherwise "<association name>_id".
516
+ def resolved_tenant_column
517
+ return nil unless tenant_enabled?
518
+
519
+ (@tenant_column || "#{tenant_base_name}_id").to_sym
520
+ end
521
+
522
+ private
523
+
524
+ def tenant_base_name
525
+ @tenant_model.to_s.demodulize.underscore
526
+ end
527
+
528
+ def set_cache_defaults
529
+ @cache_backend = :memory
530
+ @cache_key_prefix = 'pf'
531
+ @cache_ttl = 3600
532
+ @conditional_set_cache_ttl = 300
533
+ @table_name_prefix = 'permission_forge_'
534
+ @uuid_generator = -> { SecureRandom.uuid }
535
+ end
536
+
537
+ def set_field_defaults
538
+ @ignored_fields = DEFAULT_IGNORED_FIELDS
539
+ @field_variant_mappings = DEFAULT_FIELD_VARIANT_MAPPINGS.dup
540
+ @extra_fields_method = :extra_fields_for_permissions
541
+ end
542
+
543
+ def set_admin_defaults
544
+ set_ui_defaults
545
+ set_gdpr_defaults
546
+ set_api_defaults
547
+ end
548
+
549
+ def set_ui_defaults
550
+ @http_basic_auth_enabled = true
551
+ @http_basic_auth_user = nil
552
+ @http_basic_auth_password = nil
553
+ @base_controller_class = '::ActionController::Base'
554
+ @on_field_filtered = nil
555
+ @unconfigured_field_strategy = :block
556
+ @response_filter_nil_user_strategy = :bypass
557
+ @custom_mask_strategies = {}
558
+ @available_locales = nil
559
+ @nil_user_strategy = :bypass
560
+ @on_rule_bypass = nil
561
+ end
562
+
563
+ def set_gdpr_defaults
564
+ @gdpr_defaults = true
565
+ @gdpr_require_explicit_choice = false
566
+ @gdpr_field_patterns = DEFAULT_GDPR_FIELD_PATTERNS.dup
567
+ @filter_parameters_integration = true
568
+ end
569
+
570
+ def set_api_defaults
571
+ @api_enabled = false
572
+ @api_auth = nil
573
+ @api_base_controller_class = '::ActionController::Base'
574
+ @api_stats_require_admin = true
575
+ @api_pagination_handler = nil
576
+ @api_max_per_page = 100
577
+ @api_default_per_page = 15
578
+ @api_record_identifier = :uuid
579
+ @api_serializer_overrides = {}
580
+ @api_action_authorization = nil
581
+ @api_assignment_authorization = nil
582
+ @api_require_authorization = true
583
+ @api_redact_rule_field_values = false
584
+ end
585
+
586
+ def default_current_user_resolver
587
+ -> { defined?(Current) ? Current.user : nil }
588
+ end
589
+
590
+ def default_user_active_check
591
+ ->(user) { user.respond_to?(:active_for_authentication?) ? user.active_for_authentication? : true }
592
+ end
593
+
594
+ def default_profile_builder
595
+ lambda { |user|
596
+ {
597
+ id: user.id,
598
+ uuid: user.try(:uuid),
599
+ name: user.try(:name),
600
+ email: user.try(:email)
601
+ }
602
+ }
603
+ end
604
+
605
+ def build_cache_store
606
+ case @cache_backend
607
+ when :rails_cache
608
+ PermissionForge::Cache::RailsCache.new(prefix: @cache_key_prefix)
609
+ when :memory
610
+ PermissionForge::Cache::MemoryCache.new(prefix: @cache_key_prefix)
611
+ when :null
612
+ PermissionForge::Cache::NullCache.new
613
+ else
614
+ if @cache_backend.is_a?(ActiveSupport::Cache::Store)
615
+ PermissionForge::Cache::RailsCache.new(prefix: @cache_key_prefix, store: @cache_backend)
616
+ else
617
+ raise ArgumentError,
618
+ "Unknown cache backend: #{@cache_backend.inspect}. " \
619
+ 'Use :rails_cache, :memory, :null, or pass an ActiveSupport::Cache::Store instance.'
620
+ end
621
+ end
622
+ end
623
+ end
624
+ end