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,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'value_casters/number'
4
+ require_relative 'value_casters/temporal'
5
+
6
+ module PermissionForge
7
+ module Operators
8
+ # What a rule value becomes for a column of each ActiveRecord type: a Ruby
9
+ # value the adapter quotes as that type, or {UNUSABLE} when the text is not
10
+ # one of that type. Each caster is a `#call(value)`; {.for} picks the one
11
+ # for a column.
12
+ #
13
+ # The notations are the same on every database and deliberately narrower
14
+ # than Ruby's own parsers, which take `4_2`, `0x5` and `NaN`.
15
+ module ValueCasters
16
+ # A value the column cannot hold. A rule with one matches nothing.
17
+ UNUSABLE = Object.new.tap { |marker| marker.define_singleton_method(:inspect) { 'UNUSABLE' } }.freeze
18
+
19
+ # What PostgreSQL's boolean input takes. The id `current_user` stands for
20
+ # is not text, and so never a boolean.
21
+ BOOLEANS = {
22
+ true => true, 'true' => true, 't' => true, 'yes' => true, 'y' => true, 'on' => true, '1' => true,
23
+ false => false, 'false' => false, 'f' => false, 'no' => false, 'n' => false, 'off' => false, '0' => false
24
+ }.freeze
25
+
26
+ NUMBER = Number::EXACT
27
+
28
+ BOOLEAN = ->(value) { BOOLEANS.fetch(value.is_a?(String) ? value.strip.downcase : value, UNUSABLE) }
29
+
30
+ # Compared as written; the id `current_user` stands for, as its text.
31
+ TEXT = ->(value) { value.nil? ? UNUSABLE : value.to_s }
32
+
33
+ # A column type without a caster (uuid, json, an array column, ...) or a
34
+ # field name that names no known column: the value is left to the database.
35
+ UNTYPED = ->(value) { value }
36
+
37
+ BY_TYPE = {
38
+ integer: NUMBER, float: NUMBER, decimal: NUMBER,
39
+ boolean: BOOLEAN,
40
+ string: TEXT, text: TEXT,
41
+ date: Temporal::DATE, time: Temporal::TIME,
42
+ datetime: Temporal::DATETIME, timestamp: Temporal::DATETIME, timestamptz: Temporal::DATETIME
43
+ }.freeze
44
+
45
+ # PostgreSQL is typed like the others, with two things left the way it
46
+ # always did them, so that no rule that works there stops working:
47
+ #
48
+ # - It reads date and time text itself and raises on what it cannot
49
+ # read. Text the Temporal casters do not recognise is left to it
50
+ # (`due_on less_than '31 Jan 2026'`, `due_on equal 'today'`); on SQLite
51
+ # and MySQL, which compare or convert such text, it stays UNUSABLE.
52
+ # - A `real` column is compared in single precision (Number::SINGLE_PRECISION).
53
+ # The SQL type is the key: its ActiveRecord type is `float` as well.
54
+ POSTGRESQL = BY_TYPE.transform_values { |caster| caster.is_a?(Temporal) ? caster.as_written : caster }
55
+ .merge('real' => Number::SINGLE_PRECISION).freeze
56
+
57
+ # Adapter name => the casters of that database, where they are not BY_TYPE.
58
+ BY_ADAPTER = { 'PostgreSQL' => POSTGRESQL, 'PostGIS' => POSTGRESQL }.freeze
59
+
60
+ # The casters that read the two bounds of `between` together, where the
61
+ # others cast one bound at a time. With nothing known about the column
62
+ # the pair is read as it was before values were typed: both integers,
63
+ # both decimals, both dates, or text.
64
+ BOUNDS = {
65
+ UNTYPED => ->(bounds) { Registry.coerce_between_values(*bounds) },
66
+ Number::SINGLE_PRECISION => Number::SINGLE_PRECISION_BOUNDS
67
+ }.freeze
68
+
69
+ # @param column [ActiveRecord::ConnectionAdapters::Column, nil]
70
+ # @param adapter_name [String, nil] the adapter of the column's model
71
+ # @return [#call] the caster for the column
72
+ def self.for(column, adapter_name = nil)
73
+ return UNTYPED if column.nil? || column.try(:array?)
74
+
75
+ casters = BY_ADAPTER.fetch(adapter_name, BY_TYPE)
76
+ casters.fetch(column.sql_type) { casters.fetch(column.type, UNTYPED) }
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Operators
5
+ # Gives a rule value the type of the column it is compared with, so no
6
+ # database is left to convert one side of a comparison.
7
+ #
8
+ # Rule values are text, and databases disagree about text compared with a
9
+ # number, a boolean or a date: PostgreSQL raises on text it cannot cast,
10
+ # SQLite compares as text, and MySQL converts — there `5 = '5_id'` and
11
+ # `0 = 'abc'` are true, and a boolean column equals `'true'` exactly when
12
+ # it is false. So the value is typed here, the same way for every database
13
+ # and for the RLS policy (Services::RlsOperatorSql):
14
+ #
15
+ # - a value the column can hold is bound as a value of its type
16
+ # ({ValueCasters}); an enum label, as the value the model stores for it;
17
+ # - a value it cannot hold is {UNUSABLE}: the rule matches nothing.
18
+ #
19
+ # PostgreSQL, which raises instead of guessing, keeps two habits of its own
20
+ # (ValueCasters::POSTGRESQL): it reads date and time text in notations of
21
+ # its own, and compares a `real` column in single precision.
22
+ #
23
+ # Only the built-in operators are typed, each by the shape of its value
24
+ # ({SHAPES}). A custom operator's value is returned as it came, and a
25
+ # column nothing is known about leaves the value to the database. A rule
26
+ # whose field_name is not a name (RuleColumn#malformed?) has no column to
27
+ # compare with: its value is UNUSABLE whatever the operator.
28
+ module ValueTyping
29
+ UNUSABLE = ValueCasters::UNUSABLE
30
+ CURRENT_USER = 'current_user'
31
+
32
+ # The shape of each built-in operator's value. The null checks take none.
33
+ SHAPES = {
34
+ Constants::EQUAL => :single, Constants::NOT_EQUAL => :single,
35
+ Constants::LESS_THAN => :single, Constants::GREATER_THAN => :single,
36
+ Constants::LESS_THAN_OR_EQUAL => :single, Constants::GREATER_THAN_OR_EQUAL => :single,
37
+ Constants::IN => :list, Constants::NOT_IN => :list, Constants::BETWEEN => :pair,
38
+ Constants::CONTAINS => :pattern, Constants::NOT_CONTAINS => :pattern,
39
+ Constants::STARTS_WITH => :pattern, Constants::ENDS_WITH => :pattern
40
+ }.freeze
41
+ # The shapes read from the rule's text as a whole, comma by comma.
42
+ SEVERAL = %i[list pair].freeze
43
+
44
+ module_function
45
+
46
+ # @param operator [String] the rule's operator
47
+ # @param value [Object] the rule's value — the user's id where
48
+ # `current_user` stood for it (see {.current_user?})
49
+ # @param target [RuleColumn] the column it is compared with
50
+ # @return [Object] what the operator's strategy binds: one value, an Array
51
+ # for `in` / `not_in` / `between`, or {UNUSABLE}
52
+ def call(operator, value, target)
53
+ return UNUSABLE if target.malformed?
54
+
55
+ shape = SHAPES[operator]
56
+ shape ? send(shape, value, target) : value
57
+ end
58
+
59
+ # `current_user` stands for the user's id wherever one value is compared.
60
+ # In a list it is an element like any other.
61
+ def current_user?(operator, value)
62
+ value == CURRENT_USER && SEVERAL.exclude?(SHAPES[operator])
63
+ end
64
+
65
+ # The caster for the column's type; an enum label is first replaced by
66
+ # the value the model stores for it.
67
+ #
68
+ # @return [#call]
69
+ def caster(target)
70
+ typed = target.caster
71
+ labels = target.enum
72
+ return typed unless labels
73
+
74
+ ->(value) { typed.call(labels.fetch(value.is_a?(String) ? value.strip : value, value)) }
75
+ end
76
+
77
+ def single(value, target)
78
+ caster(target).call(value)
79
+ end
80
+
81
+ # A LIKE pattern is built from the text of the value, whatever the column.
82
+ def pattern(value, _target)
83
+ ValueCasters::TEXT.call(value)
84
+ end
85
+
86
+ # Every element has to be a value of the column; a list of none is no list.
87
+ def list(value, target)
88
+ elements = split(value)
89
+ elements.empty? ? UNUSABLE : all_typed(elements, caster(target))
90
+ end
91
+
92
+ # Exactly two bounds, each a value of the column — or read together, by
93
+ # the casters that do so (ValueCasters::BOUNDS).
94
+ def pair(value, target)
95
+ bounds = split(value)
96
+ return UNUSABLE unless bounds.size == 2
97
+
98
+ typed = caster(target)
99
+ together = ValueCasters::BOUNDS[typed]
100
+ together ? together.call(bounds) : all_typed(bounds, typed)
101
+ end
102
+
103
+ def split(value)
104
+ value.to_s.split(',').map(&:strip)
105
+ end
106
+
107
+ def all_typed(elements, caster)
108
+ typed = elements.map { |element| caster.call(element) }
109
+ typed.include?(UNUSABLE) ? UNUSABLE : typed
110
+ end
111
+
112
+ private_class_method :caster, :single, :pattern, :list, :pair, :split, :all_typed
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ # Boot warnings for the three query-scoping levels (called by BootChecks).
5
+ #
6
+ # @api private
7
+ module QueryScopingWarnings
8
+ module_function
9
+
10
+ # Level 2 in :raise mode is a dev/test/CI guard; raising in production
11
+ # turns a missed call site into an outage. Allowed, but never silently.
12
+ def warn_unscoped_detection_raise_in_production(pf_config)
13
+ return unless pf_config.unscoped_query_detection == :raise
14
+ return unless pf_config.unscoped_query_detection_environments.include?('production')
15
+
16
+ Rails.logger&.warn '[PermissionForge] WARNING: unscoped_query_detection = :raise is enabled ' \
17
+ 'for production. A single unscoped query will raise UnscopedQueryError ' \
18
+ 'in live traffic — prefer :log in production and :raise in test/CI.'
19
+ end
20
+
21
+ # Level 1 applies filter-type rules only; join-type rules still require
22
+ # permission_scoped. Surface that gap when both are in play.
23
+ def warn_join_rules_not_auto_scoped(pf_config)
24
+ return unless pf_config.auto_query_scoping_enabled?
25
+
26
+ join_rules = PermissionForge::PermissionSetRule.join_type_rules
27
+ return unless join_rules.exists?
28
+
29
+ Rails.logger&.warn '[PermissionForge] SECURITY warning: auto_query_scoping is enabled but ' \
30
+ "#{join_rules.count} join-type rule(s) exist. Join rules are NOT applied " \
31
+ 'by auto-scoping — queries relying on them must go through ' \
32
+ 'Model.permission_scoped.'
33
+ rescue ActiveRecord::StatementInvalid
34
+ # Tables don't exist yet (pre-migration)
35
+ end
36
+
37
+ # Level 3 needs Postgres; on any other adapter the session variable and
38
+ # policies simply don't exist, so enabling it is a silent no-op — warn.
39
+ # On Postgres, compare live policy digests against the current rule state
40
+ # so edited rules can't silently diverge from the installed policies.
41
+ def warn_rls_misconfiguration(pf_config)
42
+ return unless pf_config.database_row_level_security
43
+
44
+ unless PermissionForge::RLS.postgres?
45
+ Rails.logger&.warn '[PermissionForge] WARNING: database_row_level_security is enabled but ' \
46
+ 'the database adapter is not PostgreSQL. RLS policies cannot be enforced — ' \
47
+ 'row-level security falls back to application-level scoping only.'
48
+ return
49
+ end
50
+
51
+ warn_rls_policy_drift
52
+ rescue ActiveRecord::NoDatabaseError, ActiveRecord::ConnectionNotEstablished
53
+ # Pre-boot: adapter unknown
54
+ end
55
+
56
+ def warn_rls_policy_drift
57
+ stale = PermissionForge::RLS.drift_report.reject { |report| report[:in_sync] }
58
+ return if stale.empty?
59
+
60
+ Rails.logger&.warn '[PermissionForge] SECURITY warning: RLS policies are missing or stale for ' \
61
+ "table(s): #{stale.map { |report| report[:table] }.join(', ')}. " \
62
+ 'Rule rows have changed since the last policy sync (or policies were never ' \
63
+ 'installed) — run `rake permission_forge:rls:sync`.'
64
+ rescue ActiveRecord::StatementInvalid
65
+ # Tables don't exist yet (pre-migration)
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ # What every rake task of the gem does about the audit log: names who ran it
5
+ # (ACTOR=, stored as actor_label with actor_type 'system' and source 'rake'),
6
+ # records `report.generated` for the read-only ones, and logs through
7
+ # Rails.logger. The tasks' own output is untouched.
8
+ #
9
+ # PermissionForge::RakeAudit.changing('permission_forge:erasure') { ... } # warns without ACTOR
10
+ # PermissionForge::RakeAudit.reporting('permission_forge:audit', counts: ->(r) { r.counts }) { report }
11
+ #
12
+ # @api private
13
+ module RakeAudit
14
+ class << self
15
+ # A task that changes data. Without ACTOR the label is "rake <task>" and
16
+ # a warning is printed.
17
+ def changing(task, env: ENV, warn_io: $stderr, &)
18
+ warn_io.puts(missing_actor_warning(task)) if env['ACTOR'].blank? && warn_io
19
+ run(task, env, &)
20
+ end
21
+
22
+ # A read-only task: runs the block (it returns the task's result), records
23
+ # `report.generated` (task name, counts of that result) and returns the
24
+ # result — before the task prints or exits.
25
+ def reporting(task, counts:, env: ENV)
26
+ run(task, env) do
27
+ result = yield
28
+ record_report(task, counts.call(result))
29
+ result
30
+ end
31
+ end
32
+
33
+ def label(task, env = ENV)
34
+ env['ACTOR'].presence || "rake #{task}"
35
+ end
36
+
37
+ # One line in the application log.
38
+ def log(task, message)
39
+ Rails.logger&.info("[permission_forge] #{task}: #{message}")
40
+ end
41
+
42
+ private
43
+
44
+ # A report must never fail because its own event cannot be written: the
45
+ # failure is reported (Rails.error, the log) and the task goes on.
46
+ def record_report(task, counts)
47
+ PermissionForge::Audit.record('report.generated', data: { 'task' => task, 'counts' => counts.as_json })
48
+ rescue StandardError => e
49
+ PermissionForge::Audit::Failure.handle(e, strategy: :report, operation: :store, event: 'report.generated', task: task)
50
+ end
51
+
52
+ def run(task, env, &)
53
+ actor = label(task, env)
54
+ log(task, "run by #{actor}")
55
+ PermissionForge::Audit::Context.with(
56
+ actor: PermissionForge::Audit::Actor.system(actor), source: 'rake', reason: env['REASON'].presence, &
57
+ )
58
+ end
59
+
60
+ def missing_actor_warning(task)
61
+ "WARNING: ACTOR is not set; recorded as \"rake #{task}\". Usage: rake #{task} ACTOR=\"your name or ticket\""
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ # The Resource names a model class answers to: its own, and those of its
5
+ # single-table-inheritance ancestors up to base_class — nearest first.
6
+ # `VipCustomer < Customer` gives [:VipCustomer, :Customer]; a plain model
7
+ # its own name alone.
8
+ #
9
+ # Every entry the user has along the chain counts, and a subclass can only
10
+ # be narrower than its base: the rules of every entry apply together; a
11
+ # view_all / modify_all bypass skips the rules of the entry that grants it
12
+ # and no other; a field is as closed as the strictest entry that configures
13
+ # it, and an entry that does not grant read hides every field the chain
14
+ # configures. A subclass with no entry of its own is its base. (A class
15
+ # used to answer to its own name only, so a subclass of a protected model
16
+ # had no rules at all and every field of it was unconfigured, before
17
+ # 0.14.) A subclass with a table of its own is not
18
+ # STI and answers to its own name alone.
19
+ #
20
+ # The one resolver for every path: find_by / permission_scoped
21
+ # (PermissionScopedFindable), the auto scope (AutoPermissionScoping), the
22
+ # attribute guard (MaskedAttributes, AttributeAccessGuard), the response
23
+ # filter (FiltersResponseAttributes) and PermitsAttributes.
24
+ #
25
+ # @api private
26
+ module ResourceChain
27
+ # The entry of a field hidden by an unreadable entry along the chain.
28
+ HIDDEN_ENTRY = { read_access: false, edit_access: false, mask_access: false, mask_strategy: 'full' }.freeze
29
+
30
+ module_function
31
+
32
+ # @param klass [Class] a model class (any class answers to its own name)
33
+ # @return [Array<Symbol>] frozen; worked out once per class
34
+ def names(klass)
35
+ klass.instance_variable_get(:@pf_resource_chain) ||
36
+ klass.instance_variable_set(:@pf_resource_chain, compute_names(klass))
37
+ end
38
+
39
+ # The names behind a resource name: the chain of the model it names, or
40
+ # the name alone when it names no model.
41
+ #
42
+ # @param resource_name [String, Symbol]
43
+ # @return [Array<Symbol>]
44
+ def names_for(resource_name)
45
+ klass = resource_name.to_s.safe_constantize
46
+ klass.is_a?(Class) && klass.respond_to?(:base_class) ? names(klass) : [resource_name.to_sym]
47
+ end
48
+
49
+ # The user's resource entries along the chain, nearest first.
50
+ #
51
+ # @param user [#permissions_hash]
52
+ # @param names [Array<Symbol>]
53
+ # @return [Array<Hash>]
54
+ def entries(user, names)
55
+ resources = user.permissions_hash.dig(:permissions, :resources)
56
+ return [] unless resources
57
+
58
+ names.filter_map do |name|
59
+ entry = resources[name]
60
+ entry if entry.present?
61
+ end
62
+ end
63
+
64
+ # The row-level rules that apply to a class, and why none do.
65
+ #
66
+ # @return [Array(Array<Hash>, Symbol)] the rules of every entry without a
67
+ # bypass, and — when that leaves none — the nearest entry's :view_all /
68
+ # :modify_all, else :no_rules
69
+ def rules(user, names)
70
+ rules = []
71
+ bypass = nil
72
+ entries(user, names).each do |entry|
73
+ reason = bypass_reason(entry[:permissions])
74
+ if reason
75
+ bypass ||= reason
76
+ else
77
+ rules.concat(entry[:permission_set_rules] || [])
78
+ end
79
+ end
80
+ [rules, (bypass || :no_rules if rules.empty?)]
81
+ end
82
+
83
+ # Whether the entry the class answers to grants a resource flag: the
84
+ # nearest entry along the chain — the class's own, or its base's when it
85
+ # has none of its own (the same entry can_*? reads for a class with one;
86
+ # a muted base reaches it, see PermissionHashBuilder's muting phase).
87
+ # False without an entry.
88
+ #
89
+ # @param flag [Symbol] :read_access, :delete_access, …
90
+ # @return [Boolean]
91
+ def grants?(user, names, flag)
92
+ entries(user, names).first&.dig(:permissions, flag) == true
93
+ end
94
+
95
+ # @return [Symbol, nil] :view_all or :modify_all when one of them lets
96
+ # the user past the rules of an entry
97
+ def bypass_reason(permissions)
98
+ return unless permissions
99
+
100
+ if permissions[:view_all] then :view_all
101
+ elsif permissions[:modify_all] then :modify_all
102
+ end
103
+ end
104
+
105
+ # The field entry a class answers with for one attribute: the one entry
106
+ # along the chain that configures it — one entry is read as a plain
107
+ # model's, its rows decide — or, among several, the strictest; hidden
108
+ # when one of them does not grant read; nil when none configures it (or
109
+ # the user has no field_permissions).
110
+ #
111
+ # @param names [Array<Symbol>, Symbol] the chain, or one name
112
+ # @return [Hash, nil]
113
+ def field_permissions(user, names, attribute)
114
+ return unless user.respond_to?(:field_permissions)
115
+ return user.field_permissions(names, attribute) unless names.is_a?(Array)
116
+ return user.field_permissions(names.first, attribute) if names.size == 1
117
+ return strictest(names.filter_map { |name| user.field_permissions(name, attribute) }) unless user.respond_to?(:permissions_hash)
118
+
119
+ compose_field(entries(user, names), attribute.to_sym)
120
+ end
121
+
122
+ # One entry: its field entry (its rows decide). Several: hidden when one
123
+ # does not grant read, else the strictest of those that configure the
124
+ # field; nil when none does.
125
+ def compose_field(resource_entries, attribute)
126
+ field_entries = resource_entries.filter_map { |entry| entry.dig(:fields, attribute) }
127
+ return field_entries.first if resource_entries.size <= 1 || field_entries.empty?
128
+
129
+ unreadable?(resource_entries) ? HIDDEN_ENTRY.dup : strictest(field_entries)
130
+ end
131
+
132
+ # The field entries a class answers with, every configured name composed
133
+ # the same way. The one table itself when one entry has fields: read-only.
134
+ #
135
+ # @return [Hash{Symbol => Hash}]
136
+ def fields(user, names)
137
+ resource_entries = entries(user, names)
138
+ tables = resource_entries.filter_map { |entry| entry[:fields].presence }
139
+ return tables.first || {} if resource_entries.size <= 1
140
+
141
+ compose_tables(tables, unreadable?(resource_entries))
142
+ end
143
+
144
+ def compose_tables(tables, hidden)
145
+ tables.flat_map(&:keys).uniq.to_h do |name|
146
+ [name, hidden ? HIDDEN_ENTRY.dup : strictest(tables.filter_map { |table| table[name] })]
147
+ end
148
+ end
149
+
150
+ # Whether one of several entries does not grant read: a class cannot be
151
+ # more readable than an entry it answers to (a muted base read reaches
152
+ # the fields only the subclass configures).
153
+ def unreadable?(resource_entries)
154
+ resource_entries.any? { |entry| entry.dig(:permissions, :read_access) != true }
155
+ end
156
+
157
+ # Of several entries for one field: readable and editable only when every
158
+ # one grants it, masked when any does, with the first masking entry's
159
+ # strategy (in chain order). One entry: itself.
160
+ def strictest(field_entries)
161
+ return field_entries.first if field_entries.size <= 1
162
+
163
+ masked = field_entries.find { |entry| entry[:mask_access] }
164
+ { read_access: field_entries.all? { |entry| entry[:read_access] == true },
165
+ edit_access: field_entries.all? { |entry| entry[:edit_access] == true },
166
+ mask_access: !masked.nil?,
167
+ mask_strategy: (masked || field_entries.first)[:mask_strategy] }
168
+ end
169
+
170
+ # The class and its superclasses up to base_class that share its table
171
+ # (single-table inheritance: Rails' base_class ignores table_name, and a
172
+ # subclass with a table of its own holds other rows); the names they
173
+ # have. A nameless class (an anonymous subclass) answers as its ancestors
174
+ # do; a subclass renamed after its base answers once.
175
+ def compute_names(klass)
176
+ classes = [klass]
177
+ if klass.respond_to?(:base_class)
178
+ base = klass.base_class
179
+ classes << classes.last.superclass while classes.last != base && same_table?(classes.last.superclass, klass)
180
+ end
181
+ classes.filter_map(&:name).map(&:to_sym).uniq.freeze
182
+ end
183
+ private_class_method :compute_names
184
+
185
+ def same_table?(ancestor, klass)
186
+ ancestor.respond_to?(:table_name) && ancestor.table_name == klass.table_name
187
+ end
188
+ private_class_method :same_table?
189
+ end
190
+ end