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,416 @@
1
+ module PermissionForge
2
+ module Concerns
3
+ # Cache read/write for user permissions.
4
+ # Provides the shared hash navigation helpers used by ResourcePermissionChecker and FieldMaskChecker.
5
+ #
6
+ # Reads are read-through: a cache miss (first access, TTL expiry, eviction,
7
+ # store flush) transparently rebuilds the hash in the *current* request
8
+ # context, so permission checks never silently operate on an empty hash.
9
+ # Cross-user changes (an admin editing permission records) invalidate the
10
+ # affected users' entries instead of rebuilding them — the rebuild happens
11
+ # lazily in each user's own request.
12
+ #
13
+ # A permission set with activation conditions (session flag, IP
14
+ # allowlist, login hours) counts for the request that READS the hash,
15
+ # never for the one that happened to build it. The entry under the
16
+ # user's key — the base — is built from the unconditional sets and
17
+ # carries the conditional ones with their conditions
18
+ # (PermissionForge::ConditionalSets). Every read evaluates the request
19
+ # context (config.request_context_resolver) against them: when none
20
+ # passes the base is the hash; when some pass, the hash is a variant
21
+ # built from the unconditional sets plus those, under a key derived from
22
+ # the base entry — read from the store, or built read-through and
23
+ # verified against the same invalidation stamp as the base
24
+ # (PermissionsPerContext). A user without conditional sets pays nothing
25
+ # for this: one store read, no resolver call, as before. A resolver that
26
+ # raises (one that reads the request where there is none) raises on
27
+ # every read of a user with conditional sets.
28
+ #
29
+ # A rebuild can race an invalidation: it reads the permission rows, an
30
+ # admin commits and invalidates, and only then does the rebuild store what
31
+ # it read. Each invalidation therefore leaves a per-user stamp in the
32
+ # cache (Cache::Invalidation), and a rebuild only leaves its hash in the
33
+ # cache when the stamp it saw before reading the rows is still there after
34
+ # storing it (Cache::Rebuild). A cache hit never looks at the stamp.
35
+ #
36
+ # Two per-request caches would defeat that check, so a rebuild goes past
37
+ # both: it reads the permission rows outside ActiveRecord's query cache
38
+ # (the repeat would otherwise be answered with the rows of the first
39
+ # attempt) and the stamp outside the cache store's local copy (the second
40
+ # read would otherwise be answered with the first).
41
+ #
42
+ # Inside a database transaction that reads from a snapshot
43
+ # (config.snapshot_transactions) a hash is not shared at all, and the
44
+ # store is not read again by the instance that built it (see
45
+ # regenerate_permissions and PermissionsMemo).
46
+ #
47
+ # Whatever happens to the store, the request that asked gets the hash that
48
+ # was built for it. An empty hash is never a safe substitute: it denies
49
+ # every can_*? check but also empties permission_set_rules, which switches
50
+ # row-level rules off.
51
+ #
52
+ # @api public
53
+ module PermissionCacheAccessor
54
+ extend ActiveSupport::Concern
55
+ include PermissionsMemo
56
+ include PermissionsPerContext
57
+
58
+ # Returns the cached permission hash for this user, rebuilding it on a
59
+ # cache miss (read-through).
60
+ #
61
+ # Memoized on the instance: reading from the cache store deserializes the
62
+ # entire hash (for broad grants, a multi-megabyte object graph), and hot
63
+ # paths — MaskedAttributes getters, response filtering, per-field checks —
64
+ # call this method once per attribute. Without a memo each of those calls
65
+ # pays a full store round-trip + deserialize (observed: a 40s admin page).
66
+ # The memo is invalidated by (a) a different permission_cache_key (tenant
67
+ # switch), (b) any bump of PermissionForge.permissions_cache_version —
68
+ # which invalidate_permissions, regenerate_permissions, and cache-store
69
+ # swaps all perform — so no instance in this process can outlive an
70
+ # invalidation with stale data, (c) for a hash that was withheld
71
+ # from the cache inside a transaction, the end of that transaction (or
72
+ # its rollback to a savepoint), and (d) for a user with conditional
73
+ # permission sets, a request context that selects other conditional
74
+ # sets than the memoized hash was built for. #reload also drops it,
75
+ # matching AR's association-cache semantics. See PermissionsMemo.
76
+ #
77
+ # Callers must treat the returned hash as read-only; every call on the
78
+ # same instance returns the same object. Deep-dup before mutating.
79
+ #
80
+ # @return [Hash] the full permission hash including :profile, :preferences, and :permissions
81
+ # @return [Hash] empty hash only when called re-entrantly during its own
82
+ # rebuild (or when a host override of regenerate_permissions never
83
+ # reaches the gem's build)
84
+ # @raise [PermissionForge::ConfigurationError] when the request context
85
+ # resolver reads this user's permissions (PermissionsPerContext)
86
+ def permissions_hash
87
+ key = permission_cache_key
88
+ memo = pf_memoized_permissions(key)
89
+ hash = pf_memoized_permissions_for_context(key, memo) if memo
90
+ return hash if hash
91
+
92
+ version = PermissionForge.permissions_cache_version
93
+ selected = pf_shared_or_rebuilt_permissions(key)
94
+ # asked by the build itself (see pf_rebuild_permissions_read_through): {} for that call alone
95
+ return selected.permissions if selected.rebuilt == :reentrant
96
+
97
+ pf_memoize_selected_permissions(key, version, selected)
98
+ end
99
+
100
+ # Rebuilds and caches the permission hash from all active permission sets:
101
+ # the base entry, and — when the request context selects conditional
102
+ # sets — the variant for them. Returns the hash that applies to the
103
+ # context.
104
+ #
105
+ # The shared cache never keeps a hash that may be older than the latest
106
+ # invalidation (Cache::Rebuild): the entry is checked against the user's
107
+ # invalidation stamp, built once more when an invalidation overtook it,
108
+ # and removed when it cannot be verified.
109
+ #
110
+ # Inside a database transaction that reads from a snapshot
111
+ # (PermissionForge::OpenTransaction.withholding, decided by
112
+ # config.snapshot_transactions) the store is not touched at all. Under
113
+ # REPEATABLE READ or SERIALIZABLE — MySQL's default, and what a SQLite
114
+ # transaction gives — the rows a rebuild reads come from a snapshot
115
+ # that may predate an invalidation that has already finished, so the
116
+ # stamp check would pass on a stale hash. The hash stays on this
117
+ # instance, which does not read the store again until that transaction
118
+ # has ended; another instance of the user goes on reading the entry of
119
+ # the committed rows.
120
+ #
121
+ # Asking for a rebuild says that something changed, though (the way to
122
+ # refresh after update_all, insert_all or delete_all, which run no
123
+ # callbacks). Called inside such a transaction, the user is therefore
124
+ # invalidated once the outermost transaction has committed
125
+ # (Cache::Settlement) — whatever another request cached from the rows as
126
+ # they were before the commit goes with it. A read-through rebuild
127
+ # (a cache miss in permissions_hash) changed nothing: it arranges
128
+ # nothing and retires nobody else's memo.
129
+ #
130
+ # In any other transaction (PostgreSQL at READ COMMITTED) a rebuild
131
+ # reads committed rows plus the transaction's own, and is stored like
132
+ # one outside a transaction — also when the transaction then rolls
133
+ # back: invalidate after a rollback.
134
+ #
135
+ # In every case this instance keeps what it built, for the request that
136
+ # asked: a snapshot a moment old, like any request that finished just
137
+ # before the commit.
138
+ #
139
+ # @return [Hash] the hash that applies to the current request context
140
+ def regenerate_permissions
141
+ PermissionForge::Services::HostKeyGuard.verify!
142
+
143
+ key = permission_cache_key
144
+ transaction = PermissionForge::OpenTransaction.withholding
145
+ reading_through = pf_read_through_build(key)
146
+ pf_settle_permissions_after_commit(nil, transaction) unless reading_through
147
+
148
+ selected = pf_rebuild_permissions(key, transaction)
149
+ reading_through[:rebuilt] = selected if reading_through
150
+
151
+ pf_clear_permissions_memo
152
+ PermissionForge.bump_permissions_cache_version! unless transaction && reading_through
153
+ pf_memoize_permissions(selected.memo(key, PermissionForge.permissions_cache_version))
154
+ # a read-through rebuild is reported by permissions_hash, once memoized
155
+ pf_report_rebuilt(transaction, read_through: false) unless reading_through
156
+ selected.permissions
157
+ end
158
+
159
+ # Removes this user's cached permission hash so the next read rebuilds it
160
+ # (read-through) in that request's own context. Removes the ambient key,
161
+ # the tenant-agnostic key, and — when a tenant id is supplied by the
162
+ # caller (e.g., a callback on a tenant-scoped record) — that tenant's key,
163
+ # so background-job invalidations can't desynchronize from web requests;
164
+ # and the keys of the previous entry namespace (Cache::UserKeys).
165
+ # Nothing is read. The per-context variants of a user with conditional
166
+ # sets stay until their TTL: no read can reach them once the base entry
167
+ # that carries their token is gone (ConditionalSets); erasure and
168
+ # cache:clear remove them (Cache::VariantRemoval).
169
+ #
170
+ # A fresh invalidation stamp is written before the keys are removed, one
171
+ # per user whatever the tenant (an invalidation in another tenant makes a
172
+ # rebuild repeat itself, which costs time, not correctness). The store's
173
+ # answers are checked, an unconfirmed invalidation is retried, and one
174
+ # that still fails is reported through Cache::Failure — it does not
175
+ # raise (see Cache::Invalidation).
176
+ #
177
+ # Inside a database transaction that reads from a snapshot
178
+ # (config.snapshot_transactions) the invalidation is made at once and
179
+ # once more when the outermost transaction has committed
180
+ # (Cache::Settlement): between the two, another request can cache the
181
+ # rows as they still are before the commit. This instance does not read
182
+ # the store again until then.
183
+ #
184
+ # @param tenant_id [Object, nil] tenant id of the record that changed, if known
185
+ # @return [Boolean] whether the store confirmed the invalidation
186
+ def invalidate_permissions(tenant_id: nil)
187
+ # the keys and stamp of Cache::UserKeys — shared with the invalidation of many holders by id
188
+ invalidation = PermissionForge::Cache::UserKeys.invalidation(id, tenant_id: tenant_id, ambient_key: permission_cache_key,
189
+ stamp_key: pf_invalidation_stamp_key)
190
+ confirmed = PermissionForge::Cache::Invalidation.call(**invalidation)
191
+ ensure
192
+ pf_clear_permissions_memo
193
+ PermissionForge.bump_permissions_cache_version!
194
+ # after the invalidation, whatever became of it: the repeat is what counts
195
+ pf_settle_permissions_after_commit(tenant_id)
196
+ # published once the memo and the version are gone: a subscriber that
197
+ # reads permissions_hash gets the state after the invalidation
198
+ PermissionForge::Audit::Runtime.invalidated(self, tenant_id: tenant_id, confirmed: confirmed == true)
199
+ end
200
+
201
+ # What the last invalidate_permissions of this user left in the cache
202
+ # (nil when there was none, or it expired), read from the store itself.
203
+ #
204
+ # For a host app that overrides regenerate_permissions to derive a cache
205
+ # entry of its own after `super`: that write races an invalidation the
206
+ # same way. Read the stamp before `super` and again after writing the
207
+ # entry, and delete the entry when the two differ.
208
+ #
209
+ # It lives as long as a hash may: a hash stored without noticing an
210
+ # invalidation would need a rebuild that took longer than cache_ttl, and
211
+ # would expire as soon.
212
+ #
213
+ # @return [String, nil]
214
+ def permission_invalidation_stamp
215
+ PermissionForge.cache.read_fresh(pf_invalidation_stamp_key)
216
+ end
217
+
218
+ # The key of this user's base entry in the ambient tenant (or none):
219
+ # "v2:tenant_<tenant id>_user_<id>" / "v2:user_<id>" (Cache::UserKeys).
220
+ #
221
+ # @return [String]
222
+ def permission_cache_key
223
+ PermissionForge::Cache::UserKeys.hash_key(id)
224
+ end
225
+
226
+ private
227
+
228
+ # The hash for the current request context: from the store, or rebuilt
229
+ # on a miss (and never from the store while this instance withholds).
230
+ #
231
+ # @return [PermissionsPerContext::Selected]
232
+ def pf_shared_or_rebuilt_permissions(key)
233
+ entry = pf_read_shared_permissions(key) unless pf_withholding_permissions?
234
+ (entry && pf_select_shared_permissions(key, entry)) || pf_rebuild_permissions_read_through
235
+ end
236
+
237
+ # Memoizes what a read selected, and reports a rebuild. A hash read
238
+ # from the store — or a variant built for a base entry read from it —
239
+ # is memoized under `version`, the one from before the read, so an
240
+ # invalidation during the read retires it. A base rebuilt by
241
+ # regenerate_permissions under the version as it is now: a rebuild
242
+ # that was stored bumps it, which keeps the memo valid until the NEXT
243
+ # invalidation.
244
+ #
245
+ # @return [Hash] the hash
246
+ def pf_memoize_selected_permissions(key, version, selected)
247
+ rebuilt = selected.rebuilt
248
+ version = PermissionForge.permissions_cache_version if rebuilt == :base
249
+ pf_memoize_permissions(selected.memo(key, version))
250
+ # after the memo: building the event finds the hash, not a rebuild
251
+ pf_report_rebuilt(selected.transaction, read_through: true) if rebuilt
252
+ selected.permissions
253
+ end
254
+
255
+ # permissions.rebuilt (Audit::Runtime): the hash was built, read-through
256
+ # or on request, inside a withholding transaction or not.
257
+ def pf_report_rebuilt(transaction, read_through:)
258
+ PermissionForge::Audit::Runtime.rebuilt(self, read_through: read_through, withheld: !transaction.nil?)
259
+ end
260
+
261
+ # The cached hash — unless this process still owes the store an
262
+ # invalidation of this user (Cache::Backlog). The entry may then be the
263
+ # very one that should be gone, so it is not read: the hash is rebuilt.
264
+ def pf_read_shared_permissions(key)
265
+ return if PermissionForge::Cache::Invalidation.owed?(pf_invalidation_stamp_key)
266
+
267
+ PermissionForge.cache.read(key)
268
+ end
269
+
270
+ # Inside a transaction whose hashes are withheld: has the user
271
+ # invalidated when it has committed — for the tenant the caller named
272
+ # or the one that is ambient now (the commit may run after the block
273
+ # that made it ambient) — and keeps this instance away from the store
274
+ # until then.
275
+ def pf_settle_permissions_after_commit(tenant_id, transaction = PermissionForge::OpenTransaction.withholding)
276
+ return unless transaction
277
+
278
+ pf_withhold_permissions_in(transaction)
279
+ tenant_id ||= PermissionForge.ambient_tenant_id
280
+ PermissionForge::Cache::Settlement.for(transaction).add([:user, id, tenant_id]) do
281
+ invalidate_permissions(tenant_id: tenant_id)
282
+ rescue StandardError => e
283
+ PermissionForge::Cache::Failure.report(operation: :invalidate, user_id: id, error: e)
284
+ end
285
+ end
286
+
287
+ def pf_invalidation_stamp_key
288
+ PermissionForge::Cache::UserKeys.stamp_key(id)
289
+ end
290
+
291
+ # Rebuilds the hash on a cache miss. Guarded against re-entrancy: building
292
+ # the hash may read attributes on this user (profile_builder, extra fields),
293
+ # and with MaskedAttributes applied those getters consult permissions_hash
294
+ # again — the guard breaks that cycle by returning {} inside the rebuild.
295
+ #
296
+ # The request gets the hash that was built and — when the shared cache
297
+ # kept it — verified, not what the store holds a moment later: another
298
+ # rebuild that raced an invalidation may have its hash there for the
299
+ # instant before it removes it.
300
+ #
301
+ # A call made while the hash for the same key is being built on this
302
+ # thread — by the build's own code: this instance, or another instance
303
+ # of the user whose build reads a masked attribute — is answered {}
304
+ # marked :reentrant, which permissions_hash returns to that call alone
305
+ # and never memoizes. A host override of regenerate_permissions that
306
+ # returns without building the hash for this key raises: {} would deny
307
+ # every check and switch row-level rules off on every call.
308
+ #
309
+ # What the build hands back to this method is kept beside it, on this
310
+ # thread, never on the instance: one user instance may be read on
311
+ # several threads at once (handed to threads of its request, or kept
312
+ # across requests), and their rebuilds would take or clear each other's
313
+ # result — the one that lost it would be left with {}, and keep it.
314
+ #
315
+ # @return [PermissionsPerContext::Selected]
316
+ # @raise [PermissionForge::ConfigurationError] when no hash was built
317
+ def pf_rebuild_permissions_read_through
318
+ key = permission_cache_key
319
+ outcome = pf_guarding_permission_build(key) do |build|
320
+ regenerate_permissions
321
+ # a host override that ran super on another thread or fiber: super memoized it
322
+ build[:rebuilt] || pf_memoized_permissions(key)&.then { |memo| PermissionsPerContext::Selected.of_memo(memo) } ||
323
+ raise(PermissionForge::ConfigurationError,
324
+ "regenerate_permissions built no permission hash for #{key}: a host override must call super, " \
325
+ 'under the same tenant')
326
+ end
327
+ outcome == :reentrant ? PermissionsPerContext::REENTRANT : outcome
328
+ end
329
+
330
+ # Runs a build of the hash for `key` on this thread (fiber), one at a
331
+ # time: a call made while one runs gets :reentrant instead.
332
+ #
333
+ # @yieldparam build [Hash] where regenerate_permissions leaves what it built
334
+ # @return [Object, :reentrant] what the block returns
335
+ def pf_guarding_permission_build(key)
336
+ building = PermissionForge::ExecutionState.fetch(BUILDING_KEY) { {} }
337
+ return :reentrant if building[key]
338
+
339
+ build = building[key] = { instance: self }
340
+ begin
341
+ yield build
342
+ ensure
343
+ building.delete(key)
344
+ end
345
+ end
346
+
347
+ # The read-through build this instance is making on this thread (fiber)
348
+ # of the hash for `key`, the key regenerate_permissions builds for: an
349
+ # override that changed the tenant before super builds for another key,
350
+ # which fills no build (pf_rebuild_permissions_read_through then raises).
351
+ def pf_read_through_build(key)
352
+ build = PermissionForge::ExecutionState[BUILDING_KEY]&.[](key)
353
+ build if build && build[:instance].equal?(self)
354
+ end
355
+
356
+ # Effective TTL for this user's cached hash:
357
+ # - config.cache_ttl as the baseline (default 1 hour),
358
+ # - clamped to conditional_set_cache_ttl when any set has activation
359
+ # conditions (the backstop for the base entry and its variants),
360
+ # - clamped to the soonest assignment expiry so an expired assignment can
361
+ # never keep granting from cache beyond its expires_at.
362
+ def pf_permission_cache_ttl(permission_sets, soonest = pf_soonest_assignment_expiry)
363
+ config = PermissionForge.configuration
364
+ [config.cache_ttl, pf_conditional_set_ttl(config, permission_sets), pf_expiry_ttl(soonest)].compact.min
365
+ end
366
+
367
+ def pf_conditional_set_ttl(config, permission_sets)
368
+ return nil unless config.conditional_set_cache_ttl && permission_sets.any?(&:conditional?)
369
+
370
+ config.conditional_set_cache_ttl
371
+ end
372
+
373
+ # When the soonest of the user's active assignments ends (nil: none does).
374
+ #
375
+ # @return [Time, nil]
376
+ def pf_soonest_assignment_expiry
377
+ permission_set_users.active_assignments.where.not(expires_at: nil).minimum(:expires_at)
378
+ end
379
+
380
+ # Seconds until `soonest`, so an expired assignment can never keep
381
+ # granting from cache beyond its expires_at.
382
+ def pf_expiry_ttl(soonest)
383
+ [(soonest - Time.current).ceil, 1].max if soonest
384
+ end
385
+
386
+ # The active, non-expired sets of the user — every one of them: which
387
+ # of the conditional ones count is decided per read (ConditionalSets).
388
+ #
389
+ # @return [Array<PermissionForge::PermissionSet>]
390
+ def pf_active_non_expired_permission_sets
391
+ active_psu_ids = permission_set_users.active_assignments.select(:permission_set_id)
392
+ # Deterministic order so "first mask_strategy wins" in FieldPermissionMerger
393
+ # is stable across requests and DB adapters (was DB-insertion-order dependent).
394
+ PermissionForge::PermissionSet.active.where(id: active_psu_ids).order(:created_at, :id).to_a
395
+ end
396
+
397
+ # The 0.13 name of pf_active_non_expired_permission_sets, kept for host
398
+ # code that calls it with `send` (a hash built by hand, as the cache
399
+ # would hold it). The gem never calls it, so a host method of this name
400
+ # changes nothing here. Drop in 1.0.
401
+ def active_non_expired_permission_sets = pf_active_non_expired_permission_sets
402
+
403
+ def pf_resource_name_permissions(resource)
404
+ resource_sym = resource.is_a?(Symbol) ? resource : resource.to_s.to_sym
405
+ permissions_hash.dig(:permissions, :resources, resource_sym)
406
+ end
407
+
408
+ def pf_resource_permissions(resource)
409
+ entry = pf_resource_name_permissions(resource)
410
+ return if entry.blank?
411
+
412
+ entry[:permissions]
413
+ end
414
+ end
415
+ end
416
+ end