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,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Audit
5
+ # The runtime decision events of the gem — what each one carries — for
6
+ # the code that makes the decisions (PermissionScopedFindable,
7
+ # AutoPermissionScoping, ControllerActionGuard, the services). Every
8
+ # method returns at once, before anything is built, when the event is
9
+ # neither stored nor heard (Audit.instrument?).
10
+ #
11
+ # Payloads name the model and the record by class (record_type: the base
12
+ # class) and id only — never through a host record's readers, which a
13
+ # masked model guards. The user the decision was made for is
14
+ # subject_user_id, the actor is Actor.current (the request's actor), and
15
+ # the live user object goes to in-process subscribers as
16
+ # payload[:objects][:user].
17
+ #
18
+ # What happens on every query under auto-scoping is published at most once
19
+ # per request (an executor run) per event, model, reason and user (#once?):
20
+ # the memo lives in the request's isolated state (ExecutionState), never
21
+ # on a record, is emptied with the audit context at the end of every
22
+ # executor run that started without it (a request, a job) and by
23
+ # Audit.reset!.
24
+ #
25
+ # @api private
26
+ module Runtime
27
+ BYPASSED = 'rule.bypassed'
28
+ DENIED = 'access.denied'
29
+ ACTION_DENIED = 'action.denied'
30
+ UNSCOPED = 'query.unscoped'
31
+
32
+ # Why row-level rules (or the action guard) did not apply.
33
+ BYPASS_REASONS = %w[view_all modify_all nil_user scope_suspended auto_scope_opt_out no_rules guard_bypass].freeze
34
+
35
+ # Asked on every relation built under auto-scoping.
36
+ BYPASS_GATE = Gate.for(BYPASSED)
37
+
38
+ ONCE_KEY = :pf_audit_runtime_once
39
+ # A thread that never ends an executor run (a script, a daemon loop)
40
+ # forgets what it published past this many keys.
41
+ ONCE_LIMIT = 1_000
42
+
43
+ class << self
44
+ # rule.bypassed: the rules (or the guard) were skipped, and why.
45
+ #
46
+ # @param reason [String, Symbol] one of BYPASS_REASONS
47
+ # @param model [Class, String] the model (or Resource name)
48
+ # @param via [String] find_by, permission_scoped, auto_scope, rule_applier, action_guard
49
+ # @param user [Object, nil] the user the decision was made for
50
+ # @param once [Boolean] at most once per request
51
+ # @param data [Hash] more event data (string keys)
52
+ # @raise [ArgumentError] for a reason outside BYPASS_REASONS
53
+ def bypassed(reason, model:, via:, user: nil, once: false, data: nil) # rubocop:disable Metrics/ParameterLists
54
+ return unless BYPASS_GATE.open? && Audit.instrument?(BYPASSED)
55
+
56
+ reason = reason.to_s
57
+ raise ArgumentError, "unknown bypass reason #{reason.inspect} (#{BYPASS_REASONS.join(', ')})" unless BYPASS_REASONS.include?(reason)
58
+ return if once && !once?(BYPASSED, model, reason, via, user)
59
+
60
+ publish(BYPASSED, model: model, user: user, record_id: nil,
61
+ data: { 'reason' => reason, 'via' => via }.merge(data || {}))
62
+ end
63
+
64
+ # access.denied (rows of a model) or action.denied (a controller
65
+ # action): the decision, why, and the record when it is known.
66
+ #
67
+ # @param event [String] DENIED or ACTION_DENIED
68
+ # @param reason [String, Symbol] why (rule, nil_user, a guard reason)
69
+ # @param model [Class, String]
70
+ # @param via [String]
71
+ # @param user [Object, nil]
72
+ # @param record_id [Object, nil] the denied record's id, when known
73
+ # @param once [Boolean] at most once per request
74
+ # @param data [Hash] more event data (string keys)
75
+ def denied(event, reason:, model:, via:, user: nil, record_id: nil, once: false, data: nil) # rubocop:disable Metrics/ParameterLists
76
+ return unless Audit.instrument?(event)
77
+ return if once && !once?(event, model, reason.to_s, via, user)
78
+
79
+ publish(event, model: model, user: user, record_id: record_id,
80
+ data: { 'reason' => reason.to_s, 'via' => via }.merge(data || {}))
81
+ end
82
+
83
+ # query.unscoped: Level 2 detection saw a query on a protected table
84
+ # that went past the rules. Once per request per table.
85
+ #
86
+ # @param model [Class, String, nil] the protected model, when known
87
+ # @param table [String]
88
+ # @param data [Hash]
89
+ def unscoped(model:, table:, data: nil)
90
+ return unless Audit.instrument?(UNSCOPED)
91
+ return unless once?(UNSCOPED, model || table, table, nil, nil)
92
+
93
+ user = current_user
94
+ publish(UNSCOPED, model: model || table, user: user, record_id: nil, data: { 'table' => table }.merge(data || {}))
95
+ end
96
+
97
+ # permissions.rebuilt (the `cache` group): a user's permission hash
98
+ # was built — read through on a cache miss, or asked for — and whether
99
+ # it was withheld from the cache inside a transaction.
100
+ def rebuilt(user, read_through:, withheld:)
101
+ cache_event('permissions.rebuilt', user, 'read_through' => read_through, 'withheld' => withheld)
102
+ end
103
+
104
+ # permissions.invalidated (the `cache` group): a user's cached hash
105
+ # was removed, for which tenant, and whether the store confirmed it.
106
+ def invalidated(user, tenant_id:, confirmed:)
107
+ cache_event('permissions.invalidated', user, 'tenant_id' => tenant_id, 'confirmed' => confirmed)
108
+ end
109
+
110
+ # Whether this is the first time in this request that `key` was
111
+ # published; remembers it.
112
+ def once?(*key)
113
+ key[-1] = user_id(key[-1])
114
+ seen = (state[ONCE_KEY] ||= {})
115
+ return false if seen.key?(key)
116
+
117
+ seen.clear if seen.size >= ONCE_LIMIT
118
+ seen[key] = true
119
+ end
120
+
121
+ # Forgets what was published once (end of an executor run, tests).
122
+ def reset!
123
+ state.delete(ONCE_KEY)
124
+ end
125
+
126
+ # The user's id, read as its primary key; nil for no user or an
127
+ # object that has none.
128
+ def user_id(user)
129
+ user.id if user.respond_to?(:id)
130
+ end
131
+
132
+ # The model's base class name (a Resource name as given).
133
+ def record_type(model)
134
+ return model.to_s unless model.is_a?(Class)
135
+
136
+ model.respond_to?(:base_class) ? model.base_class.name : model.name
137
+ end
138
+
139
+ # The model's own name (a subclass stays itself).
140
+ def model_name(model)
141
+ model.is_a?(Class) ? model.name : model.to_s
142
+ end
143
+
144
+ private
145
+
146
+ # Whose hash: record and subject are the user.
147
+ def cache_event(event, user, data)
148
+ return unless Audit.instrument?(event)
149
+
150
+ Audit.instrument(event) do
151
+ { record_type: record_type(user.class), record_id: user_id(user), subject_user_id: user_id(user), data: data,
152
+ objects: { user: user } }
153
+ end
154
+ end
155
+
156
+ def publish(event, model:, user:, record_id:, data:)
157
+ Audit.instrument(event) do
158
+ { record_type: record_type(model), record_id: record_id, subject_user_id: user_id(user),
159
+ data: { 'model' => model_name(model) }.merge(data), objects: { user: user } }
160
+ end
161
+ end
162
+
163
+ # A user the resolver raises for is no user here: the decision being
164
+ # reported has been made already, and Actor.current reports the
165
+ # resolver's failure when the event is built.
166
+ def current_user
167
+ PermissionForge.current_user
168
+ rescue StandardError
169
+ nil
170
+ end
171
+
172
+ def state
173
+ ExecutionState
174
+ end
175
+ end
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Audit
5
+ # How Audit.instrument emits a runtime (access) event once it knows the
6
+ # event is stored or heard. Extended into Audit; uses its #write, #notify
7
+ # and #listening?.
8
+ #
9
+ # Published at once — the decision has been made, whatever becomes of the
10
+ # transaction it was made in — never after a commit. Stored at once, or,
11
+ # inside an open transaction, once the outermost transaction has ended,
12
+ # committed or rolled back (a denial inside a transaction the denial
13
+ # itself rolls back is still recorded); the notification then says
14
+ # `stored: false, deferred: true`.
15
+ #
16
+ # Re-entrancy: nothing is done while this thread (fiber) is already
17
+ # building, storing or publishing a runtime event (#emitting?). A payload
18
+ # or a subscriber that reads a masked attribute or runs a permission
19
+ # check gets its answer, but publishes no event about it — and cannot
20
+ # loop.
21
+ #
22
+ # @api private
23
+ module RuntimeEmission
24
+ # Set while this fiber builds, stores or publishes a runtime event — a
25
+ # fiber-local ExecutionState slot: the emission runs in the fiber that
26
+ # started it.
27
+ EMITTING_KEY = :pf_audit_emitting
28
+
29
+ # @return [Boolean] whether this fiber is building, storing or
30
+ # publishing a runtime event
31
+ def emitting?
32
+ ExecutionState[EMITTING_KEY] == true
33
+ end
34
+
35
+ # Hands { data:, objects: } to the gem's light subscribers
36
+ # (Gate#direct_listeners) with the re-entrancy guard, building no event.
37
+ # For a caller that has the payload at hand (Audit::FieldAccess).
38
+ #
39
+ # @return [nil]
40
+ def emit_light(entry, listeners, light)
41
+ return if emitting?
42
+
43
+ emitting { deliver_light(entry, listeners, light) }
44
+ nil
45
+ end
46
+
47
+ # One field decision handed to the light subscribers (the legacy
48
+ # on_field_filtered hook) as it is made (Audit::FieldAccess::Direct).
49
+ #
50
+ # @return [nil]
51
+ def emit_decision(listeners, resource_name, field_name, action, user)
52
+ return if emitting?
53
+
54
+ emitting do
55
+ listeners.each { |listener| listener.deliver_decision(resource_name, field_name, action, user) }
56
+ end
57
+ nil
58
+ end
59
+
60
+ private
61
+
62
+ # The legacy hooks are the only listeners and nothing is stored
63
+ # (Gate#direct_listeners): they get { data:, objects: } straight away —
64
+ # no Recorder, no actor, no notification.
65
+ #
66
+ # @return [nil]
67
+ def instrument_light(entry, listeners, payload)
68
+ return if emitting?
69
+
70
+ emitting do
71
+ payload = runtime_payload(entry, payload) { yield if block_given? }
72
+ unless payload.equal?(FAILED)
73
+ light = { event: entry.name, data: payload[:data] || {}, objects: payload[:objects] || Recorder::NO_OBJECTS }
74
+ deliver_light(entry, listeners, light)
75
+ end
76
+ end
77
+ nil
78
+ end
79
+
80
+ def deliver_light(entry, listeners, light)
81
+ listeners.each { |listener| listener.deliver_light(entry.notification, light) }
82
+ rescue StandardError => e
83
+ Failure.handle(e, strategy: configuration.audit_hook_failure_strategy, operation: :publish, event: entry.name)
84
+ end
85
+
86
+ # @return [AuditEvent, nil]
87
+ def instrument_runtime(entry, payload, store:)
88
+ return if emitting?
89
+
90
+ emitting do
91
+ payload = runtime_payload(entry, payload) { yield if block_given? }
92
+ emit_runtime(Recorder.new(entry, payload), store: store) unless payload.equal?(FAILED)
93
+ end
94
+ end
95
+
96
+ def emitting(&)
97
+ ExecutionState.with(EMITTING_KEY, true, &)
98
+ end
99
+
100
+ # The caller's payload with the block's merged in, source 'runtime'
101
+ # unless given. A block that raises follows audit_hook_failure_strategy.
102
+ def runtime_payload(entry, payload)
103
+ more = yield
104
+ payload = payload.merge(more) if more
105
+ payload.merge(source: payload[:source] || 'runtime')
106
+ rescue StandardError => e
107
+ Failure.handle(e, strategy: configuration.audit_hook_failure_strategy, operation: :build, event: entry.name)
108
+ FAILED
109
+ end
110
+
111
+ def emit_runtime(recorder, store:)
112
+ transaction = OpenTransaction.current(PermissionForge::AuditEvent) if store
113
+ stored = write(recorder, store: store && transaction.nil?, strategy: configuration.audit_hook_failure_strategy,
114
+ change: false)
115
+ return if stored.equal?(FAILED)
116
+
117
+ store_runtime_later(transaction, recorder) if transaction
118
+ notify(recorder.entry, recorder.notification_payload(stored, deferred: !transaction.nil?)) if listening?(recorder.entry)
119
+ stored
120
+ end
121
+
122
+ # OpenTransaction#after_end: once, when the outermost transaction has
123
+ # ended either way (a savepoint that rolls back hands it on). It runs
124
+ # from Rails' commit or rollback, where raising would replace the
125
+ # caller's own exception: a failure is reported, never raised, whatever
126
+ # the strategy (as Audit::Detached does).
127
+ def store_runtime_later(transaction, recorder)
128
+ transaction.after_end do
129
+ strategy = configuration.audit_hook_failure_strategy
130
+ emitting { write(recorder, store: true, strategy: strategy == :raise ? :report : strategy, change: false) }
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Audit
5
+ # Records an `audit.settings_observed` event when the audit settings in
6
+ # force differ from those of the most recent such event, so "was auditing
7
+ # ever off, and since when" can be answered from the log. The engine calls
8
+ # it at boot (not in the test environment); it never raises.
9
+ #
10
+ # Only the settings at boot are observed: a toggle made at runtime after
11
+ # boot is not seen until the next boot or an explicit call.
12
+ #
13
+ # Lambdas are recorded as "set" / "unset" only, never their code.
14
+ #
15
+ # PermissionForge::Audit::SettingsObserver.call # => AuditEvent | nil
16
+ #
17
+ # @api private
18
+ module SettingsObserver
19
+ EVENT = 'audit.settings_observed'
20
+ OPTIONS = %i[
21
+ audit_events audit_actor_resolver audit_request_context audit_change_failure_strategy
22
+ audit_hook_failure_strategy audit_runtime_events audit_log_subscriber audit_retention_period
23
+ audit_log_authorization admin_assignment_authorization
24
+ ].freeze
25
+
26
+ class << self
27
+ # @return [AuditEvent, nil] the new event; nil when nothing changed,
28
+ # the table is missing, auditing is off or recording failed
29
+ def call(configuration: PermissionForge.configuration)
30
+ return unless PermissionForge::AuditEvent.table_exists?
31
+
32
+ settings = fingerprint(configuration)
33
+ return if last_observed == settings
34
+
35
+ payload = { data: { 'settings' => settings }, actor: { type: 'system', label: 'boot' }, source: 'service' }
36
+ return PermissionForge::Audit.record(EVENT, **payload) unless configuration.audit_events == false
37
+
38
+ # Auditing is off: Audit.record would store nothing, yet "was auditing
39
+ # ever off" is what this event answers. Stored directly, not published.
40
+ Recorder.new(EventCatalog.fetch(EVENT), payload).store!
41
+ rescue StandardError => e
42
+ Rails.logger&.warn("[PermissionForge] could not record the audit settings: #{e.class}: #{e.message}") if defined?(Rails)
43
+ nil
44
+ end
45
+
46
+ # @return [Hash{String => Object}] stable, JSON-safe values
47
+ def fingerprint(configuration = PermissionForge.configuration)
48
+ OPTIONS.to_h { |option| [option.to_s, value_of(configuration.public_send(option))] }
49
+ end
50
+
51
+ private
52
+
53
+ def value_of(value)
54
+ case value
55
+ when nil then 'unset'
56
+ when Array then value.map(&:to_s).sort
57
+ when ActiveSupport::Duration, Numeric then value.to_i
58
+ when Symbol then value.to_s
59
+ when String, true, false then value
60
+ else 'set' # a lambda, or anything else: only that it is configured
61
+ end
62
+ end
63
+
64
+ def last_observed
65
+ PermissionForge::AuditEvent.unscoped.where(event: EVENT).order(occurred_at: :desc, id: :desc).first&.data&.[]('settings')
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Audit
5
+ # Where an event came from: the source column. The admin UI, the API and
6
+ # jobs say so through the audit context (Audit::Context); a process that
7
+ # says nothing is a console, a rake task, or else a service.
8
+ #
9
+ # @api private
10
+ module Source
11
+ SOURCES = %w[admin_ui api rake job console service runtime].freeze
12
+
13
+ class << self
14
+ # @return [String] the context's source, else the detected one
15
+ def current
16
+ Context[:source] || detect
17
+ end
18
+
19
+ # @param console [Boolean] whether this process is `rails console`
20
+ # @param rake_tasks [Array<String>] the tasks rake was started with
21
+ # @return [String]
22
+ def detect(console: process_console?, rake_tasks: process_rake_tasks)
23
+ return 'console' if console
24
+ return 'rake' if rake_tasks.any?
25
+
26
+ 'service'
27
+ end
28
+
29
+ # @raise [ArgumentError] for a source outside the contract
30
+ # @return [String]
31
+ def validate!(value)
32
+ source = value.to_s
33
+ raise ArgumentError, "audit source must be one of #{SOURCES.join(', ')} (got #{value.inspect})" unless SOURCES.include?(source)
34
+
35
+ source
36
+ end
37
+
38
+ private
39
+
40
+ def process_console?
41
+ defined?(::Rails::Console) ? true : false
42
+ end
43
+
44
+ # Rake is loaded in a test process too: only a process started with a
45
+ # task is a rake task.
46
+ def process_rake_tasks
47
+ return [] unless defined?(::Rake::Application) && ::Rake.respond_to?(:application)
48
+
49
+ ::Rake.application.top_level_tasks
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Audit
5
+ module Subscribers
6
+ # A notification subscriber of the gem, switched by an option of the
7
+ # configuration: installed once (Audit.install!), silenced while the
8
+ # option is off. ActiveSupport asks #silenced? before it delivers, so a
9
+ # silenced subscriber costs nothing, and the option can change at run
10
+ # time. The option's writer calls Gate.changed!, which is how Audit::Gate
11
+ # knows the answer without asking every time.
12
+ #
13
+ # Subclasses name what they subscribe to (#pattern), when they are
14
+ # silenced, and what they do with an event's payload (#deliver).
15
+ class Base
16
+ # @return [String, Regexp] the notification(s) to subscribe to
17
+ def pattern
18
+ raise NotImplementedError
19
+ end
20
+
21
+ # @param _name [String] the notification
22
+ # @return [Boolean]
23
+ def silenced?(_name)
24
+ raise NotImplementedError
25
+ end
26
+
27
+ # Whether it can be handed the light payload ({ data:, objects: }) in
28
+ # place of a built event (Audit::Gate#direct_listeners).
29
+ def light? = false
30
+
31
+ # The light payload, from Audit.instrument (the hook path that builds
32
+ # no event).
33
+ def deliver_light(name, payload)
34
+ deliver(name, payload)
35
+ end
36
+
37
+ # @return [Object] the ActiveSupport subscriber, for Subscribers.uninstall!
38
+ def subscribe!
39
+ ActiveSupport::Notifications.subscribe(pattern, self)
40
+ end
41
+
42
+ # ActiveSupport's timed subscriber interface.
43
+ def call(name, _started, _finished, _id, payload)
44
+ deliver(name, payload)
45
+ end
46
+
47
+ private
48
+
49
+ # @param name [String] "<event>.permission_forge"
50
+ # @param payload [Hash] see Audit::Recorder#notification_payload
51
+ def deliver(name, payload)
52
+ raise NotImplementedError
53
+ end
54
+
55
+ def configuration
56
+ PermissionForge.configuration
57
+ end
58
+
59
+ # A hook that raises follows config.audit_hook_failure_strategy: it
60
+ # never breaks the request unless the strategy is :raise.
61
+ def guarded(event, hook)
62
+ yield
63
+ rescue StandardError => e
64
+ Failure.handle(e, strategy: configuration.audit_hook_failure_strategy, operation: :hook, event: event, hook: hook)
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Audit
5
+ module Subscribers
6
+ # The two hooks that existed before the audit event log, kept with the
7
+ # call signatures they always had, as subscribers of the runtime events
8
+ # that replaced them. Installed always and silenced while the hook is
9
+ # nil, so a hook set at run time is called from then on.
10
+ #
11
+ # Alone (no other listener, the group not stored) they are handed the
12
+ # decision without an event being built (Audit::Gate#direct_listeners):
13
+ # a hook costs what it cost before the audit event log, roughly.
14
+ #
15
+ # A hook that raises no longer only logs: the failure follows
16
+ # config.audit_hook_failure_strategy (:report by default — logged and
17
+ # handed to Rails.error; :raise raises it).
18
+ module LegacyHooks
19
+ # config.on_rule_bypass(user:, resource:, reason: :view_all_or_modify_all)
20
+ # — called when view_all or modify_all lets `find_by` / `find_by!` /
21
+ # `permission_scoped` skip the row-level rules, as before. Not called
22
+ # for auto-scoping (it would fire on every query) nor for the other
23
+ # bypass reasons, which only rule.bypassed reports.
24
+ class RuleBypass < Base
25
+ REASONS = %w[view_all modify_all].freeze
26
+ VIA = %w[find_by permission_scoped].freeze
27
+ LEGACY_REASON = :view_all_or_modify_all
28
+
29
+ def pattern = 'rule.bypassed.permission_forge'
30
+
31
+ def light? = true
32
+
33
+ def silenced?(_name)
34
+ configuration.on_rule_bypass.nil?
35
+ end
36
+
37
+ private
38
+
39
+ def deliver(_name, payload)
40
+ hook = configuration.on_rule_bypass
41
+ data = payload[:data]
42
+ return unless hook && REASONS.include?(data['reason']) && VIA.include?(data['via'])
43
+
44
+ guarded('rule.bypassed', :on_rule_bypass) do
45
+ hook.call(user: payload[:objects][:user], resource: data['model'], reason: LEGACY_REASON)
46
+ end
47
+ end
48
+ end
49
+
50
+ # config.on_field_filtered(resource_name:, field_name:, action:, user:)
51
+ # — called once for every field decision, in the order the decisions
52
+ # were made, as before: one field.access event (a record read)
53
+ # carries all of them in payload[:objects][:decisions]. Each call is
54
+ # guarded on its own, so one that raises does not stop the others.
55
+ class FieldFiltered < Base
56
+ def pattern = 'field.access.permission_forge'
57
+
58
+ def light? = true
59
+
60
+ def silenced?(_name)
61
+ configuration.on_field_filtered.nil?
62
+ end
63
+
64
+ # One decision, as it is made (Audit::FieldAccess::Direct).
65
+ def deliver_decision(resource_name, field_name, action, user)
66
+ hook = configuration.on_field_filtered
67
+ return unless hook
68
+
69
+ resource_name = resource_name.is_a?(Symbol) ? resource_name.name : resource_name.to_s
70
+ guarded('field.access', :on_field_filtered) do
71
+ hook.call(resource_name: resource_name, field_name: field_name, action: action, user: user)
72
+ end
73
+ end
74
+
75
+ private
76
+
77
+ def deliver(_name, payload)
78
+ hook = configuration.on_field_filtered
79
+ return unless hook
80
+
81
+ Array(payload[:objects][:decisions]).each do |decision|
82
+ next unless decision.hook
83
+
84
+ guarded('field.access', :on_field_filtered) do
85
+ hook.call(resource_name: decision.resource_name, field_name: decision.field_name,
86
+ action: decision.action, user: decision.user)
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+
93
+ register(:on_rule_bypass, ->(_config) { LegacyHooks::RuleBypass.new.subscribe! })
94
+ register(:on_field_filtered, ->(_config) { LegacyHooks::FieldFiltered.new.subscribe! })
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Audit
5
+ module Subscribers
6
+ # config.audit_log_subscriber = true: one structured line per audit
7
+ # event (stored or not) in Rails.logger, at info level —
8
+ #
9
+ # [PermissionForge] audit {"event":"access.denied","category":"access","actor_type":"user","actor_id":"7",...}
10
+ #
11
+ # The line carries ids, never the name snapshots (actor_label,
12
+ # record_label): logs usually travel further than the audit table.
13
+ # Silenced while the option is off; notifications that are not audit
14
+ # events (cache_failure.permission_forge) are left alone.
15
+ class Log < Base
16
+ SUFFIX = '.permission_forge'
17
+ PREFIX = '[PermissionForge] audit '
18
+ OMITTED = %i[actor_label record_label audit_event objects].freeze
19
+
20
+ def pattern = /\.permission_forge\z/
21
+
22
+ def silenced?(_name)
23
+ !configuration.audit_log_subscriber
24
+ end
25
+
26
+ private
27
+
28
+ def deliver(name, payload)
29
+ return unless configuration.audit_log_subscriber && defined?(Rails) && Rails.logger
30
+
31
+ event = name.delete_suffix(SUFFIX)
32
+ return unless EventCatalog.registered?(event)
33
+
34
+ guarded(event, :audit_log_subscriber) { Rails.logger.info(PREFIX + line(payload).to_json) }
35
+ end
36
+
37
+ def line(payload)
38
+ payload.except(*OMITTED).compact
39
+ end
40
+ end
41
+
42
+ register(:log, ->(_config) { Log.new.subscribe! })
43
+ end
44
+ end
45
+ end