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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Api
5
+ module Serializers
6
+ class PermissionSetUserSerializer < BaseSerializer
7
+ attributes :id, :uuid, :permission_set_id, :user_id, :expires_at, :created_at, :updated_at
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Api
5
+ module Serializers
6
+ class ResourceActionSerializer < BaseSerializer
7
+ attributes :id, :uuid, :resource_id, :name, :label, :description,
8
+ :is_permitted, :is_active,
9
+ :created_at, :updated_at
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Api
5
+ module Serializers
6
+ class ResourceFieldSerializer < BaseSerializer
7
+ attributes :id, :uuid, :resource_id, :name, :label, :description,
8
+ :is_active, :is_editable,
9
+ :read_access, :edit_access, :mask_access, :mask_strategy,
10
+ :created_at, :updated_at
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Api
5
+ module Serializers
6
+ class ResourceSerializer < BaseSerializer
7
+ attributes :id, :uuid, :permission_set_id, :name, :description,
8
+ :is_active, :is_editable,
9
+ :read_access, :create_access, :edit_access, :delete_access,
10
+ :view_all, :modify_all,
11
+ :created_at, :updated_at
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Audit
5
+ # Who did it, as stored on an event: the type (user, api, basic_auth,
6
+ # system, unknown), the id as a string (bigint and UUID keys both fit) and
7
+ # a label — the person's name as it was at the time.
8
+ #
9
+ # Resolution order (Actor.current): the actor in the audit context (an
10
+ # explicit `PermissionForge.with_audit_actor` block, or the request
11
+ # context the controllers set), then `PermissionForge.current_user`,
12
+ # otherwise unknown.
13
+ #
14
+ # @api public
15
+ class Actor
16
+ TYPES = %w[user api basic_auth system unknown].freeze
17
+
18
+ # The actor's type (`user`, `system`, `unknown`, ...), id and display label, as stored on the event.
19
+ attr_reader :type, :id, :label
20
+
21
+ # The class of the user object the actor was made from (for_user), nil
22
+ # for an actor given as a Hash or built by hand. Not part of equality:
23
+ # it says whether the id is a key of config.audit_user_class
24
+ # (HasAuditTrail stamps created_by / updated_by only then).
25
+ attr_reader :user_class
26
+
27
+ # @raise [ArgumentError] for a type outside TYPES
28
+ def initialize(type:, id: nil, label: nil, user_class: nil)
29
+ type = type.to_s
30
+ raise ArgumentError, "audit actor type must be one of #{TYPES.join(', ')} (got #{type.inspect})" unless TYPES.include?(type)
31
+
32
+ @type = type
33
+ @id = id&.to_s
34
+ @label = label.presence&.to_s
35
+ @user_class = user_class
36
+ freeze
37
+ end
38
+
39
+ class << self
40
+ # @return [Actor] never nil
41
+ def current
42
+ Context[:actor] || from_current_user || unknown
43
+ end
44
+
45
+ # @param user [Object] a host user (anything with an id)
46
+ # @param label [String, nil] overrides the name snapshot
47
+ # @param type [String] 'user' unless the user acts as an API principal
48
+ def for_user(user, label: nil, type: 'user')
49
+ new(type: type, id: user.id, label: label || label_for(user), user_class: record_class_of(user))
50
+ end
51
+
52
+ # The class of the record behind a decorated user (SimpleDelegator /
53
+ # Delegator, Draper), so a host's decorated current_user still stamps.
54
+ def record_class_of(user)
55
+ inner = user
56
+ 3.times do
57
+ if inner.respond_to?(:__getobj__) then inner = inner.__getobj__
58
+ elsif inner.respond_to?(:decorated?) && inner.respond_to?(:object) && inner.decorated? then inner = inner.object
59
+ else break
60
+ end
61
+ end
62
+ inner.class
63
+ end
64
+
65
+ # @return [Actor] the actor used when nothing identifies who acted
66
+ def unknown
67
+ new(type: 'unknown')
68
+ end
69
+
70
+ # @param label [String] what ran (a job, a rake task)
71
+ # @return [Actor] a `system` actor
72
+ def system(label)
73
+ new(type: 'system', label: label)
74
+ end
75
+
76
+ # An Actor, a Hash { type:, id:, label: }, a user, or nil.
77
+ #
78
+ # @return [Actor, nil]
79
+ def coerce(value)
80
+ case value
81
+ when nil then nil
82
+ when Actor then value
83
+ when Hash then new(**value.symbolize_keys.slice(:type, :id, :label))
84
+ else for_user(value)
85
+ end
86
+ end
87
+
88
+ # The name snapshot for a user: config.user_label_formatter, else the
89
+ # same readers the admin screens use (Admin::PersonLabel).
90
+ def label_for(user)
91
+ PermissionForge::Admin::PersonLabel.new(user).name
92
+ end
93
+
94
+ private
95
+
96
+ def from_current_user
97
+ user = resolve_current_user
98
+ for_user(user) if user
99
+ end
100
+
101
+ # A current_user_resolver that raises (a Current attribute reset, a
102
+ # session store that is down) must not turn every change into an
103
+ # error: the actor is unknown and the failure is reported
104
+ # (config.audit_hook_failure_strategy; :raise raises). A user it
105
+ # returns that cannot be read is a failure of the event itself.
106
+ def resolve_current_user
107
+ PermissionForge.current_user
108
+ rescue StandardError => e
109
+ Failure.handle(e, strategy: PermissionForge.configuration.audit_hook_failure_strategy, operation: :actor)
110
+ nil
111
+ end
112
+ end
113
+
114
+ # Whether the actor is a record of `klass` (or a subclass): made from a
115
+ # user object of that class, not merely carrying an id.
116
+ #
117
+ # @param klass [Class, nil]
118
+ def user_of?(klass)
119
+ !klass.nil? && type == 'user' && !id.nil? && user_class.is_a?(Class) && user_class <= klass
120
+ end
121
+
122
+ # The three actor columns of an event.
123
+ def to_columns
124
+ { actor_type: type, actor_id: id, actor_label: label }
125
+ end
126
+
127
+ # [type, id, label]
128
+ def to_a
129
+ [type, id, label]
130
+ end
131
+
132
+ # Two actors are equal when their type, id and label are.
133
+ def ==(other)
134
+ other.is_a?(Actor) && to_a == other.to_a
135
+ end
136
+ alias eql? ==
137
+
138
+ # @return [Integer] consistent with {#==}
139
+ def hash
140
+ to_a.hash
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Audit
5
+ # Who acts in an admin UI or API request — never taken from what the
6
+ # client sends (parameters, headers), only from what the host and the
7
+ # engine have verified:
8
+ #
9
+ # 1. config.audit_actor_resolver, called with the controller (a user, an
10
+ # Actor or a Hash { type:, id:, label: }; nil: it does not know)
11
+ # 2. the signed-in host user (config.current_user_resolver)
12
+ # 3. admin UI under HTTP Basic: the username that was checked, as
13
+ # actor_type 'basic_auth' with no id
14
+ #
15
+ # Nobody named: the request's fallback — unknown in the admin UI, an
16
+ # anonymous API client in the API (api_auth said yes, but not who).
17
+ #
18
+ # A step that raises, or answers with something that is no actor, makes
19
+ # the actor unknown and is reported (Audit::Failure, under
20
+ # config.audit_hook_failure_strategy): the request goes on.
21
+ #
22
+ # @api private
23
+ class ActorResolver
24
+ # The request env key under which BasicAuthentication leaves the
25
+ # username it has just verified. Not reachable from a header (those are
26
+ # HTTP_*).
27
+ BASIC_AUTH_ENV = 'permission_forge.audit.basic_auth_user'
28
+ API_CLIENT_LABEL = 'API client'
29
+
30
+ # The steps tried, in order, per request source; the first that names
31
+ # someone wins.
32
+ STEPS = {
33
+ 'admin_ui' => %i[configured_actor current_user basic_auth_user],
34
+ 'api' => %i[configured_actor current_user]
35
+ }.freeze
36
+
37
+ # Who acts when no step names anyone.
38
+ FALLBACKS = {
39
+ 'admin_ui' => -> { Actor.unknown },
40
+ 'api' => -> { Actor.new(type: 'api', label: API_CLIENT_LABEL) }
41
+ }.freeze
42
+
43
+ # @param controller [ActionController::Metal]
44
+ # @param source [String] 'admin_ui' or 'api'
45
+ # @param fallback [Boolean] false: unknown instead of the source's
46
+ # fallback when nobody is named (a refused request)
47
+ # @return [Actor] never nil
48
+ def self.call(controller, source:, fallback: true, configuration: PermissionForge.configuration)
49
+ new(controller, configuration).call(source, fallback: fallback)
50
+ end
51
+
52
+ def initialize(controller, configuration)
53
+ @controller = controller
54
+ @configuration = configuration
55
+ end
56
+
57
+ # @return [Actor]
58
+ def call(source, fallback: true)
59
+ identify(source) || (fallback ? FALLBACKS.fetch(source).call : Actor.unknown)
60
+ rescue StandardError => e
61
+ Failure.handle(e, strategy: @configuration.audit_hook_failure_strategy, operation: :actor, source: source)
62
+ Actor.unknown
63
+ end
64
+
65
+ # Who the request names, without a fallback. Raises what a step raises.
66
+ #
67
+ # @return [Actor, nil]
68
+ def identify(source)
69
+ STEPS.fetch(source).each do |step|
70
+ actor = send(step)
71
+ return actor if actor
72
+ end
73
+ nil
74
+ end
75
+
76
+ private
77
+
78
+ def configured_actor
79
+ resolver = @configuration.audit_actor_resolver
80
+ coerce(resolver.call(@controller)) if resolver
81
+ end
82
+
83
+ def current_user
84
+ coerce(PermissionForge.current_user)
85
+ end
86
+
87
+ # Set by BasicAuthentication only after the credentials matched.
88
+ def basic_auth_user
89
+ username = @controller.request.env[BASIC_AUTH_ENV]
90
+ Actor.new(type: 'basic_auth', label: username) if username.present?
91
+ end
92
+
93
+ # A user without an id names nobody; anything Actor.coerce cannot read
94
+ # raises (and so counts as a failure).
95
+ def coerce(value)
96
+ actor = Actor.coerce(value)
97
+ return if actor.nil? || (actor.type == 'user' && actor.id.nil?)
98
+
99
+ actor
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Audit
5
+ # The read-only admin pages that leave a trace: who looked at someone's
6
+ # effective access, who read or exported the audit trail. One registry,
7
+ # keyed by "controller_path#action"; the base controllers record the
8
+ # first entry that applies after a successful (2xx) response — the
9
+ # controllers themselves are not touched.
10
+ #
11
+ # Adding a page is one call:
12
+ #
13
+ # AuditedViews.register('permission_forge/audit_events#index', 'audit.viewed',
14
+ # only_if: ->(c) { c.request.format.html? },
15
+ # payload: ->(c) { { data: { 'filters' => c.params.to_unsafe_h.slice('from', 'to') } } })
16
+ #
17
+ # `only_if` (optional) decides whether the entry applies to the request;
18
+ # `payload` (optional) returns the keys passed to Audit.record (record:,
19
+ # subject_user_id:, permission_set:, data: …). Actor, source, request id
20
+ # and IP come from the request's audit context.
21
+ #
22
+ # @api private
23
+ module AuditedViews
24
+ View = Struct.new(:event, :only_if, :payload, keyword_init: true) do
25
+ def applies?(controller)
26
+ only_if.nil? || only_if.call(controller) ? true : false
27
+ end
28
+
29
+ def payload_for(controller)
30
+ payload ? payload.call(controller) : {}
31
+ end
32
+ end
33
+
34
+ @entries = {}
35
+
36
+ class << self
37
+ # @param key [String] "controller_path#action", e.g. 'permission_forge/people#show'
38
+ # @param event [String] an admin, change or system event of the catalogue
39
+ # @param only_if [#call, nil] lambda(controller) → whether the entry applies
40
+ # @param payload [#call, nil] lambda(controller) → Hash for Audit.record
41
+ # @raise [EventCatalog::UnknownEventError, ArgumentError]
42
+ def register(key, event, only_if: nil, payload: nil)
43
+ entry = EventCatalog.fetch(event)
44
+ raise ArgumentError, "#{event} is an access event: views record admin events" if entry.runtime?
45
+
46
+ (@entries[key.to_s] ||= []) << View.new(event: entry.name, only_if: only_if, payload: payload)
47
+ end
48
+
49
+ # Removes every entry of a key, or only those of one event.
50
+ def unregister(key, event = nil)
51
+ views = @entries[key.to_s]
52
+ return unless views
53
+
54
+ event ? views.reject! { |view| view.event == event } : views.clear
55
+ @entries.delete(key.to_s) if views.empty?
56
+ end
57
+
58
+ # @return [Hash{String => Array<View>}]
59
+ def entries
60
+ @entries.transform_values(&:dup)
61
+ end
62
+
63
+ # @return [Array<View>]
64
+ def entries_for(key)
65
+ Array(@entries[key.to_s]).dup
66
+ end
67
+
68
+ # @return [View, nil] the first entry that applies to the request
69
+ def match(controller)
70
+ views = @entries["#{controller.controller_path}##{controller.action_name}"]
71
+ views&.find { |view| view.applies?(controller) }
72
+ end
73
+
74
+ # Records the view when the response succeeded and an entry applies.
75
+ # A HEAD request shows nothing and is not recorded.
76
+ #
77
+ # @return [AuditEvent, nil]
78
+ def record(controller)
79
+ return if controller.request.head? || !controller.response.successful?
80
+
81
+ view = match(controller)
82
+ Audit.record(view.event, **view.payload_for(controller)) if view
83
+ end
84
+ end
85
+
86
+ # Shared pieces of the built-in entries. What was shown is decided by
87
+ # the response (its media type), not by what the client asked for: an
88
+ # Accept header can negotiate a CSV without a .csv in the path.
89
+ module Requests
90
+ AUDIT_FILTERS = %w[who type days sort].freeze
91
+ LOG_FILTERS = %w[from to event category actor_type actor_id subject_user_id permission_set correlation_id
92
+ record_type record_id record_uuid].freeze
93
+ API_FILTERS = %w[from to event category actor_type actor_id subject_user_id permission_set record_type record_id
94
+ record_uuid correlation_id page per_page].freeze
95
+ VALUE_LIMIT = 255
96
+
97
+ module_function
98
+
99
+ def csv?(controller)
100
+ controller.response.media_type == Mime[:csv].to_s
101
+ end
102
+
103
+ # 'csv' / 'json' for a file the response handed over, nil for a page.
104
+ def export_format(controller)
105
+ { Mime[:csv].to_s => 'csv', Mime[:json].to_s => 'json' }[controller.response.media_type]
106
+ end
107
+
108
+ def export?(controller)
109
+ !export_format(controller).nil?
110
+ end
111
+
112
+ def tab?(controller, tab)
113
+ !csv?(controller) && controller.params[:tab] == tab
114
+ end
115
+
116
+ # A parameter as a String: a scalar, or the first scalar of an
117
+ # array (`?explain_user_id[]=7` — what `find_by(id: [...])` uses),
118
+ # capped; nil for anything else.
119
+ def scalar(value)
120
+ value = value.first if value.is_a?(Array)
121
+ value.to_s[0, VALUE_LIMIT] if value.is_a?(String) || value.is_a?(Numeric)
122
+ end
123
+
124
+ def id_param(controller, key)
125
+ scalar(controller.params[key])
126
+ end
127
+
128
+ def person(controller, data = {})
129
+ { subject_user_id: id_param(controller, :id), data: data }
130
+ end
131
+
132
+ def permission_set(controller, data = {})
133
+ { record: find_set(id_param(controller, :id)), data: data }
134
+ end
135
+
136
+ def find_set(id)
137
+ PermissionForge::PermissionSet.find_by(id: id) if id
138
+ end
139
+
140
+ def filters(controller, names)
141
+ names.to_h { |name| [name, scalar(controller.params[name])] }.compact_blank
142
+ end
143
+
144
+ def audit_filters(controller)
145
+ filters(controller, AUDIT_FILTERS)
146
+ end
147
+ end
148
+
149
+ requests = Requests
150
+
151
+ # A person's page: effective access tab, audit tab, the CSV of their changes.
152
+ register('permission_forge/people#show', 'audit.exported',
153
+ only_if: requests.method(:csv?),
154
+ payload: ->(controller) { requests.person(controller, 'scope' => 'person', 'format' => 'csv') })
155
+ register('permission_forge/people#show', 'admin.effective_access_viewed',
156
+ only_if: ->(controller) { requests.tab?(controller, 'effective') },
157
+ payload: ->(controller) { requests.person(controller, 'scope' => 'person') })
158
+ register('permission_forge/people#show', 'audit.viewed',
159
+ only_if: ->(controller) { requests.tab?(controller, 'audit') },
160
+ payload: ->(controller) { requests.person(controller, 'scope' => 'person') })
161
+
162
+ # A Permission Set's audit tab and its CSV export.
163
+ register('permission_forge/permission_sets#show', 'audit.exported',
164
+ only_if: requests.method(:csv?),
165
+ payload: lambda { |controller|
166
+ requests.permission_set(controller, 'scope' => 'permission_set', 'format' => 'csv', 'filters' => requests.audit_filters(controller))
167
+ })
168
+ register('permission_forge/permission_sets#show', 'audit.viewed',
169
+ only_if: ->(controller) { requests.tab?(controller, 'audit') },
170
+ payload: lambda { |controller|
171
+ requests.permission_set(controller, 'scope' => 'permission_set', 'filters' => requests.audit_filters(controller))
172
+ })
173
+
174
+ # The field drawer's "explain for a person": that person's effective access to the field.
175
+ # Same test as the controller's (`params[:explain_user_id].present?`), so whatever it explains is recorded.
176
+ register('permission_forge/resource_fields#show', 'admin.effective_access_viewed',
177
+ only_if: ->(controller) { controller.params[:explain_user_id].present? },
178
+ payload: lambda { |controller|
179
+ { subject_user_id: requests.id_param(controller, :explain_user_id),
180
+ permission_set: requests.find_set(requests.id_param(controller, :permission_set_id)),
181
+ data: { 'scope' => 'field', 'resource' => controller.params[:resource_model].to_s, 'field' => controller.params[:key].to_s } }
182
+ })
183
+
184
+ # The audit log pages (AuditEventsController): the filtered list, its
185
+ # CSV / JSON export, one event, and the "who holds what" export.
186
+ register('permission_forge/audit_events#index', 'audit.exported',
187
+ only_if: requests.method(:export?),
188
+ payload: lambda { |controller|
189
+ { data: { 'scope' => 'log', 'format' => requests.export_format(controller),
190
+ 'filters' => requests.filters(controller, Requests::LOG_FILTERS) } }
191
+ })
192
+ register('permission_forge/audit_events#index', 'audit.viewed',
193
+ payload: lambda { |controller|
194
+ { data: { 'scope' => 'log', 'filters' => requests.filters(controller, Requests::LOG_FILTERS + %w[views page]) } }
195
+ })
196
+ register('permission_forge/audit_events#show', 'audit.viewed',
197
+ payload: ->(controller) { { data: { 'scope' => 'event', 'identifier' => requests.id_param(controller, :id) } } })
198
+ register('permission_forge/audit_events#holdings', 'audit.exported',
199
+ payload: ->(controller) { { data: { 'scope' => 'holdings', 'format' => requests.export_format(controller) } } })
200
+
201
+ # The JSON API's audit log (Api::AuditEventsController): a page of events, or one.
202
+ register('permission_forge/api/audit_events#index', 'audit.viewed',
203
+ payload: ->(controller) { { data: { 'scope' => 'api', 'filters' => requests.filters(controller, Requests::API_FILTERS) } } })
204
+ register('permission_forge/api/audit_events#show', 'audit.viewed',
205
+ payload: ->(controller) { { data: { 'scope' => 'api', 'identifier' => requests.id_param(controller, :identifier) } } })
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Audit
5
+ # The audit boot warnings, written by the engine once the database is
6
+ # reachable. The missing-table warning is logged by Audit (once per
7
+ # process, by Audit.verify_table!) with the text of .missing_table; this
8
+ # adds auditing switched off and one shared login for every change.
9
+ #
10
+ # @api private
11
+ module BootWarnings
12
+ def self.call(configuration = PermissionForge.configuration)
13
+ PermissionForge::Audit.verify_table!
14
+
15
+ if configuration.audit_events == false
16
+ Rails.logger&.warn '[PermissionForge] SECURITY warning: audit_events is false, so permission changes ' \
17
+ 'are not recorded. Set config.audit_events = :auto unless another system records them.'
18
+ end
19
+
20
+ return unless configuration.http_basic_auth_enabled && configuration.audit_actor_resolver.nil?
21
+
22
+ Rails.logger&.warn '[PermissionForge] The admin UI uses HTTP Basic auth and audit_actor_resolver is not ' \
23
+ 'set: every change is attributed to the shared login. Set ' \
24
+ 'config.audit_actor_resolver to return the signed-in admin.'
25
+ end
26
+
27
+ # The missing-table warning (Audit logs it once per process). Its
28
+ # remedy depends on the app: a migration of the gem's that creates the
29
+ # table is there (a fresh install, or an upgrade already generated) and
30
+ # has not run — db:migrate; none — the install predates the audit log,
31
+ # and the upgrade generator writes it (on a fresh install it would
32
+ # duplicate migrations).
33
+ #
34
+ # @param table [String] the audit events table name
35
+ # @return [String]
36
+ def self.missing_table(table)
37
+ remedy = if audit_migration_present?
38
+ 'the pending migration has run (bin/rails db:migrate)'
39
+ else
40
+ 'its migration is generated and run (bin/rails generate permission_forge:upgrade, then bin/rails db:migrate)'
41
+ end
42
+ "[PermissionForge] audit_events is :auto but the table '#{table}' was not found: no audit events are " \
43
+ "recorded until #{remedy}. Changes still work."
44
+ end
45
+
46
+ # @return [Boolean] whether a permission_forge migration in the app's
47
+ # migration paths creates the audit events table
48
+ def self.audit_migration_present?
49
+ Array(ActiveRecord::Migrator.migrations_paths).any? do |path|
50
+ Dir.glob(File.join(path.to_s, '*permission_forge*.rb')).any? { |file| File.read(file).include?('audit_events') }
51
+ end
52
+ rescue SystemCallError
53
+ false
54
+ end
55
+ private_class_method :audit_migration_present?
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Audit
5
+ # The one cleaning of every string an event stores in a column of its own
6
+ # — whoever supplied it: a request (reason, a typed Basic username, a
7
+ # request id), the host (a resolver's label, with_audit_actor, a rake
8
+ # ACTOR or REASON) or the gem. Audit::Recorder applies it to every event
9
+ # it builds.
10
+ #
11
+ # - valid UTF-8 (invalid bytes replaced), so a row is never refused
12
+ # - one line: tab, newline and the other line breaks become a space;
13
+ # NUL (PostgreSQL refuses it) and every other C0/C1 control character
14
+ # (terminal escapes) are removed
15
+ # - at most LIMIT characters: the columns are varchar(255), and MySQL
16
+ # in strict mode refuses — not truncates — a longer value, which
17
+ # would let a client keep its own refusal out of the log
18
+ #
19
+ # `data` (JSON, escaped by the encoder) is left as it is.
20
+ #
21
+ # @api private
22
+ module ColumnValues
23
+ LIMIT = 255
24
+ COLUMNS = %i[event category actor_type actor_id actor_label source record_type record_id record_uuid record_label
25
+ permission_set_uuid subject_user_id reason request_id ip correlation_id].freeze
26
+ LINE_BREAKS = /[\t\n\v\f\r\u0085

]/
27
+ CONTROLS = /[\u0000-\u001F\u007F-\u009F]/
28
+
29
+ class << self
30
+ # @param row [Hash] the event's attributes
31
+ # @return [Hash] a copy with every String in COLUMNS cleaned
32
+ def clean(row)
33
+ row.to_h { |column, value| [column, COLUMNS.include?(column) ? value(value) : value] }
34
+ end
35
+
36
+ # @return [String, Object] a cleaned String; anything else unchanged
37
+ def value(value)
38
+ return value unless value.is_a?(String)
39
+
40
+ utf8(value).gsub(LINE_BREAKS, ' ').gsub(CONTROLS, '')[0, LIMIT]
41
+ end
42
+
43
+ private
44
+
45
+ def utf8(value)
46
+ text = if [Encoding::UTF_8, Encoding::BINARY].include?(value.encoding)
47
+ value.dup.force_encoding(Encoding::UTF_8)
48
+ else
49
+ value.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
50
+ end
51
+ text.scrub
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end