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,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ # @api private
5
+ module ApiAuthentication
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ before_action :verify_api_enabled!
10
+ before_action :authenticate_api_request!
11
+ end
12
+
13
+ private
14
+
15
+ def verify_api_enabled!
16
+ return if PermissionForge.configuration.api_enabled
17
+
18
+ head :not_found
19
+ end
20
+
21
+ # A refused request records api.auth_failed: who the host knows (the
22
+ # audit actor resolver, the signed-in user), else unknown.
23
+ def authenticate_api_request!
24
+ auth_check = PermissionForge.configuration.api_auth
25
+ if auth_check.nil?
26
+ audit_api_auth_failure('api_auth_not_configured', 401)
27
+ return head(:unauthorized)
28
+ end
29
+ return if auth_check.call(self)
30
+
31
+ audit_api_auth_failure('api_auth_denied', 403)
32
+ render json: { error: { status: 403, messages: I18n.t('permission_forge.api.errors.forbidden') } }, status: :forbidden
33
+ end
34
+
35
+ def audit_api_auth_failure(reason, status)
36
+ PermissionForge::Audit::RequestContext.refused(self, 'api.auth_failed', source: 'api',
37
+ data: { 'reason' => reason, 'status' => status })
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,97 @@
1
+ module PermissionForge
2
+ # Asks `config.admin_assignment_authorization` before a change to who holds
3
+ # a Permission Set or belongs to a Permission Set Group — in the HTML admin
4
+ # and in the API (there after `api_assignment_authorization`, which keeps
5
+ # its own signature). Segregation of duties lives in the lambda, e.g. no
6
+ # self-assignment:
7
+ #
8
+ # config.admin_assignment_authorization = lambda { |controller, target, user_ids, action|
9
+ # admin = PermissionForge.current_user
10
+ # admin&.admin? && !user_ids.include?(admin.id)
11
+ # }
12
+ #
13
+ # The lambda receives (controller, target, user_ids, action):
14
+ # controller — the admin or API controller handling the request
15
+ # target — the PermissionForge::PermissionSet or ::PermissionSetGroup
16
+ # user_ids — Array of the people concerned, cast to the user model's
17
+ # primary-key type (Integers for bigint keys)
18
+ # action — one of ACTIONS
19
+ # A truthy answer allows; nil (no lambda) allows everything. A refused
20
+ # request changes nothing, so it writes no change event; a raising lambda
21
+ # raises (the request fails, nothing changes).
22
+ #
23
+ # @api private
24
+ module AssignmentAuthorization
25
+ # :grant / :revoke a Permission Set, :change_expiry of an assignment,
26
+ # :review (access review "Keep"), :add_member / :remove_member of a group.
27
+ ACTIONS = %i[grant revoke change_expiry review add_member remove_member].freeze
28
+
29
+ private
30
+
31
+ # @param target [PermissionForge::PermissionSet, PermissionForge::PermissionSetGroup]
32
+ # @param user_ids [Array, Object] the people concerned
33
+ # @param action [Symbol] one of ACTIONS
34
+ # @return [Boolean]
35
+ def pf_assignment_allowed?(target, user_ids, action)
36
+ raise ArgumentError, "unknown assignment action #{action.inspect}" unless ACTIONS.include?(action)
37
+
38
+ hook = PermissionForge.configuration.admin_assignment_authorization
39
+ return true unless hook
40
+
41
+ hook.call(self, target, pf_cast_user_ids(user_ids), action) ? true : false
42
+ end
43
+
44
+ # HTML admin: true (after redirecting with an alert) when any of the
45
+ # targets is refused, so the caller returns before changing anything.
46
+ # The refusal is recorded (admin.auth_failed).
47
+ def pf_assignment_refused?(targets, user_ids, action, path)
48
+ refused = Array(targets).find { |target| !pf_assignment_allowed?(target, user_ids, action) }
49
+ return false unless refused
50
+
51
+ pf_audit_assignment_refusal('admin.auth_failed', refused, user_ids, action)
52
+ redirect_to path, alert: t('permission_forge.flash.assignment_not_authorized')
53
+ true
54
+ end
55
+
56
+ # API: true (after rendering a 403) when the target is refused. The
57
+ # refusal is recorded (api.auth_failed).
58
+ def pf_assignment_refused_api?(target, user_ids, action)
59
+ return false if pf_assignment_allowed?(target, user_ids, action)
60
+
61
+ pf_audit_assignment_refusal('api.auth_failed', target, user_ids, action)
62
+ render_not_authorized(I18n.t('permission_forge.api.errors.assignment_not_authorized'))
63
+ true
64
+ end
65
+
66
+ # The locked set that refuses a change to its holders because the change
67
+ # widens access — adding to a granting set, removing from a Muting one,
68
+ # an end date moved the matching way (Admin::EditableLock.widening_lock);
69
+ # nil when the change may go ahead.
70
+ #
71
+ # @param change [Symbol] :grant, :revoke or :change_expiry (with from:/to:)
72
+ # @return [PermissionForge::PermissionSet, nil]
73
+ def pf_widening_lock(permission_set, change, **expiry)
74
+ PermissionForge::Admin::EditableLock.widening_lock(permission_set, change, **expiry)
75
+ end
76
+
77
+ # HTML admin: true (after redirecting with the lock's alert and recording
78
+ # admin.lock_refused) when `locked` (a #pf_widening_lock answer) is a set.
79
+ def pf_widening_refused?(locked, path)
80
+ return false unless locked
81
+
82
+ alert = locked.is_muting ? t('permission_forge.flash.locked_widening_muting') : t('permission_forge.flash.locked_widening')
83
+ pf_reject_locked!(locked, path, alert: alert)
84
+ end
85
+
86
+ # pf_audit_refused: the including base controller's (HTML or API).
87
+ def pf_audit_assignment_refusal(event, target, user_ids, action)
88
+ payload = PermissionForge::Audit::Refusals.assignment(target, user_ids, action: action,
89
+ hook: 'admin_assignment_authorization')
90
+ pf_audit_refused(event, **payload)
91
+ end
92
+
93
+ def pf_cast_user_ids(user_ids)
94
+ PermissionForge::Admin::PeopleDirectory::Ids.cast(user_ids)
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,55 @@
1
+ module PermissionForge
2
+ # The HTML admin's gate in front of the audit log: the log page, one
3
+ # event, the exports, and the audit tabs of a person and a Permission Set.
4
+ # Audit::LogAccess decides (config.audit_log_authorization); a refusal is
5
+ # recorded once per request (admin.auth_failed, reason
6
+ # audit_log_not_authorized) and answered with a 403 — a page that says so,
7
+ # or nothing for a file. A refused request is never recorded as viewed or
8
+ # exported (Audit::AuditedViews records 2xx responses only).
9
+ #
10
+ # @api private
11
+ module AuditLogGate
12
+ REFUSAL_REASON = 'audit_log_not_authorized'.freeze
13
+
14
+ private
15
+
16
+ # @return [Boolean] whether this request may read the audit log
17
+ def pf_audit_log_allowed?
18
+ return @pf_audit_log_allowed if defined?(@pf_audit_log_allowed)
19
+
20
+ @pf_audit_log_allowed = PermissionForge::Audit::LogAccess.allowed?(self)
21
+ pf_audit_refused('admin.auth_failed', data: { 'reason' => REFUSAL_REASON }) unless @pf_audit_log_allowed
22
+ @pf_audit_log_allowed
23
+ end
24
+
25
+ # before_action: the refusal, unless the log may be read.
26
+ def pf_require_audit_log!
27
+ pf_render_audit_refusal unless pf_audit_log_allowed?
28
+ end
29
+
30
+ # The audit tab of a page (people#show, permission_sets#show) and its
31
+ # CSV are the audit log too; the other tabs need no permission.
32
+ def pf_audit_tab_allowed?
33
+ return true unless @tab == 'audit' || request.format.csv?
34
+
35
+ pf_audit_log_allowed?
36
+ end
37
+
38
+ # Refused on a page with tabs: the page renders with the refusal on its
39
+ # audit tab (403); a file is not sent.
40
+ def pf_refuse_audit_tab
41
+ return head(:forbidden) unless request.format.html?
42
+
43
+ @audit_refused = true
44
+ render :show, status: :forbidden
45
+ end
46
+
47
+ def pf_render_audit_refusal
48
+ if request.format.html?
49
+ render 'permission_forge/shared/audit_refused', status: :forbidden
50
+ else
51
+ head :forbidden
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,77 @@
1
+ module PermissionForge
2
+ # HTTP Basic authentication for the admin screens, driven by `config.http_basic_auth_*`;
3
+ # the engine's controllers include it.
4
+ #
5
+ # @api public
6
+ module BasicAuthentication
7
+ extend ActiveSupport::Concern
8
+
9
+ # A username is stored as the actor label of an event: a varchar(255).
10
+ AUDIT_USERNAME_LIMIT = 255
11
+
12
+ included do
13
+ before_action :authenticate_by_http_basic
14
+ end
15
+
16
+ private
17
+
18
+ def authenticate_by_http_basic
19
+ return unless http_basic_auth_enabled?
20
+
21
+ unless http_basic_auth_configured?
22
+ audit_http_basic_failure('basic_auth_not_configured')
23
+ return head(:unauthorized)
24
+ end
25
+
26
+ authenticate_or_request_with_http_basic('PermissionForge') do |given_user, given_password|
27
+ verified = http_basic_credentials_match?(given_user, given_password)
28
+ verified ? remember_http_basic_user : audit_http_basic_failure('invalid_credentials')
29
+ verified
30
+ end
31
+ end
32
+
33
+ def http_basic_credentials_match?(given_user, given_password)
34
+ ActiveSupport::SecurityUtils.secure_compare(given_user, http_basic_auth_credentials[:user].to_s) &
35
+ ActiveSupport::SecurityUtils.secure_compare(given_password, http_basic_auth_credentials[:password].to_s)
36
+ end
37
+
38
+ # The verified username names the actor of the request's audit events
39
+ # (Audit::ActorResolver) — the configured one, never what was typed.
40
+ def remember_http_basic_user
41
+ request.env[PermissionForge::Audit::ActorResolver::BASIC_AUTH_ENV] = http_basic_auth_credentials[:user].to_s
42
+ end
43
+
44
+ # Records admin.auth_failed when credentials were sent: the username as
45
+ # typed (capped, valid UTF-8), never the password. A browser's first
46
+ # request, which sends none, is the challenge, not a failure.
47
+ def audit_http_basic_failure(reason)
48
+ return unless ActionController::HttpAuthentication::Basic.has_basic_credentials?(request)
49
+
50
+ username = ActionController::HttpAuthentication::Basic.user_name_and_password(request).first
51
+ PermissionForge::Audit::RequestContext.refused(
52
+ self, 'admin.auth_failed', source: 'admin_ui',
53
+ actor: { type: 'basic_auth', label: audit_username(username) },
54
+ data: { 'reason' => reason }
55
+ )
56
+ end
57
+
58
+ def audit_username(username)
59
+ username.to_s.dup.force_encoding(Encoding::UTF_8).scrub.delete("\u0000")[0, AUDIT_USERNAME_LIMIT]
60
+ end
61
+
62
+ def http_basic_auth_enabled?
63
+ PermissionForge.configuration.http_basic_auth_enabled
64
+ end
65
+
66
+ def http_basic_auth_configured?
67
+ http_basic_auth_credentials.values.all?(&:present?)
68
+ end
69
+
70
+ def http_basic_auth_credentials
71
+ {
72
+ user: PermissionForge.configuration.http_basic_auth_user,
73
+ password: PermissionForge.configuration.http_basic_auth_password
74
+ }
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ # For admin actions that clear somebody's cached permissions: says what the
5
+ # cache store confirmed, instead of announcing success whatever happened.
6
+ #
7
+ # @api private
8
+ module CacheConfirmation
9
+ extend ActiveSupport::Concern
10
+
11
+ private
12
+
13
+ # Whether the cache store confirmed every invalidation made in the block.
14
+ # The gem's own failure bookkeeping is asked, not the value of
15
+ # invalidate_permissions: host apps override that method.
16
+ def cache_confirmed?(&)
17
+ PermissionForge::Cache::Failure.none?(&)
18
+ end
19
+
20
+ # The notice — or, when the cache store did not confirm that the cached
21
+ # permissions are gone, the alert, which says the old ones may still be
22
+ # served. `keys` names both, under permission_forge.flash.people:
23
+ # `{ notice: :erased, alert: :erased_cache_kept }`.
24
+ #
25
+ # @return [Hash] for redirect_to
26
+ def cache_flash(confirmed, keys, **)
27
+ kind = confirmed ? :notice : :alert
28
+ key = keys.fetch(kind)
29
+ { kind => t("permission_forge.flash.people.#{key}", default: key.to_s.humanize, **) }
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,29 @@
1
+ module PermissionForge
2
+ # How the admin screens and the admin API verify a mutating request. Rails
3
+ # 8.2 checks the browser's Sec-Fetch-Site header, and `load_defaults 8.2`
4
+ # selects `:header_only`, which lets a plain-HTTP request that carries no
5
+ # such header through without any check — a forged request from a browser
6
+ # that does not send it, or to an app served over HTTP. The engine's
7
+ # controllers use `:header_or_legacy_token` whatever the host chose: the
8
+ # header decides when the browser sends it (same-origin and same-site pass
9
+ # without a token, cross-site is refused), and without it the authenticity
10
+ # token is required, as on every earlier Rails. Rails before 8.2 has no
11
+ # such choice and always checks the token.
12
+ #
13
+ # @api private
14
+ module ForgeryProtection
15
+ extend ActiveSupport::Concern
16
+
17
+ STRATEGY = :header_or_legacy_token
18
+
19
+ class_methods do
20
+ # The engine's verification strategy as protect_from_forgery options,
21
+ # empty where Rails offers no choice. Each controller calls
22
+ # protect_from_forgery itself with its own `with:`, so the call stays
23
+ # visible (and Brakeman sees it).
24
+ def pf_forgery_strategy
25
+ respond_to?(:forgery_protection_verification_strategy) ? { using: STRATEGY } : {}
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,40 @@
1
+ module PermissionForge
2
+ # The admin screens answer HTML, plus the exports an action declares
3
+ # (pf_exports). Any other Accept header or extension — a scanner's
4
+ # application/json, text/csv on a form — is answered in HTML: the admin
5
+ # has no other templates, and a failing form must not end in a missing
6
+ # template (a 500). Runs first, before anything renders.
7
+ #
8
+ # @example
9
+ # pf_exports %i[csv json], only: %i[index holdings]
10
+ #
11
+ # @api private
12
+ module HtmlFormat
13
+ extend ActiveSupport::Concern
14
+
15
+ included do
16
+ # action name => the formats it answers besides HTML
17
+ class_attribute :pf_export_formats, instance_writer: false, default: {}.freeze
18
+ prepend_before_action :pf_negotiate_html
19
+ end
20
+
21
+ class_methods do
22
+ # @param formats [Array<Symbol>] e.g. %i[csv]
23
+ # @param only [Symbol, Array<Symbol>] the actions that export them
24
+ def pf_exports(formats, only:)
25
+ exports = Array(only).to_h { |action| [action.to_s, formats.map(&:to_s).freeze] }
26
+ self.pf_export_formats = pf_export_formats.merge(exports).freeze
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ # The first acceptable format that is HTML or an export of the action
33
+ # (as respond_to would choose), else HTML.
34
+ def pf_negotiate_html
35
+ exports = pf_export_formats.fetch(action_name, [])
36
+ chosen = request.formats.map { |format| format.symbol.to_s }.find { |name| name == 'html' || exports.include?(name) }
37
+ request.format = (chosen || 'html').to_sym
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,87 @@
1
+ module PermissionForge
2
+ # Request parameters in the shape an action expects. A crafted request
3
+ # that sends a String or an Array where a Hash is expected, or a Hash where
4
+ # an id or a date is, gets a 4xx (or the value is ignored) — never a 500.
5
+ #
6
+ # Every attribute hash the admin and the API write from (pf_require_hash,
7
+ # pf_hash_param) and every typed text (pf_text_param) comes without NUL
8
+ # bytes: PostgreSQL refuses one in text (a 500), the other databases would
9
+ # store it. The one place for both surfaces; `reason` keeps its own
10
+ # cleaning (Audit::ColumnValues).
11
+ #
12
+ # @api private
13
+ module ParamShapes
14
+ # Typed text: see #pf_text_param.
15
+ #
16
+ # @return [String]
17
+ def self.text(value)
18
+ value.is_a?(String) ? value.delete("\0").strip : ''
19
+ end
20
+
21
+ # `value` with NUL bytes removed from every String in it, however deep.
22
+ #
23
+ # @return [Object] a new Parameters / Array, or the value itself
24
+ def self.without_nul(value)
25
+ case value
26
+ when String then value.delete("\0")
27
+ when Array then value.map { |item| without_nul(item) }
28
+ when ActionController::Parameters then value.transform_values { |item| without_nul(item) }
29
+ else value
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ # params.require(key), which must be a Hash of attributes.
36
+ #
37
+ # @raise [ActionController::ParameterMissing] when it is missing, empty or
38
+ # not a Hash (400; the API answers 422)
39
+ def pf_require_hash(key)
40
+ value = params.require(key)
41
+ raise ActionController::ParameterMissing, key unless value.is_a?(ActionController::Parameters)
42
+
43
+ ParamShapes.without_nul(value)
44
+ end
45
+
46
+ # params[key] when it is a Hash of attributes (without NUL bytes), else
47
+ # an empty one.
48
+ def pf_hash_param(key)
49
+ value = params[key]
50
+ value.is_a?(ActionController::Parameters) ? ParamShapes.without_nul(value) : ActionController::Parameters.new
51
+ end
52
+
53
+ # Typed text from params[key]: a String without NUL bytes (they cut a
54
+ # quoted SQL literal short, and PostgreSQL refuses them in text) and
55
+ # without surrounding white space; '' for anything else.
56
+ def pf_text_param(key)
57
+ ParamShapes.text(params[key])
58
+ end
59
+
60
+ # params[:tab] when it is one of `tabs`, else nil (a Hash or an Array
61
+ # would reach url_for as unpermitted parameters).
62
+ def pf_tab_param(tabs)
63
+ tab = params[:tab]
64
+ tab if tabs.include?(tab)
65
+ end
66
+
67
+ # The Integer ids in params[key] (one value or a list); anything that is
68
+ # not digits is dropped.
69
+ def pf_id_list(key)
70
+ Array.wrap(params[key]).filter_map { |value| PermissionForge::Admin::RecordId.cast(value) }
71
+ end
72
+
73
+ # The time params[key] names, nil when it is blank.
74
+ #
75
+ # @raise [ActionController::BadRequest] for a value that is no time:
76
+ # a date that cannot be read must never become "no end date"
77
+ def pf_time_param(key)
78
+ value = params[key]
79
+ return nil if value.blank?
80
+
81
+ time = Time.zone.parse(value.to_s) if value.is_a?(String)
82
+ time || raise(ActionController::BadRequest, "#{key} is not a time")
83
+ rescue ArgumentError
84
+ raise ActionController::BadRequest, "#{key} is not a time"
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,42 @@
1
+ module PermissionForge
2
+ # The admin screens and the admin API serve one tenant at a time. With
3
+ # tenancy on (config.tenant_model) a request that reaches them with no
4
+ # current tenant is refused — acts_as_tenant scopes nothing without one, so
5
+ # it would read and change every tenant's Permission Sets, assignments and
6
+ # audit log — unless config.admin_tenantless_access = :allow. The refusal
7
+ # is recorded through the including controller's pf_audit_refused
8
+ # (admin.auth_failed / api.auth_failed, reason tenant_missing) and answered
9
+ # with a 403: JSON from the API, plain text from the admin screens (their
10
+ # layout's navigation and counts would read across tenants).
11
+ #
12
+ # The including controller registers `before_action :pf_require_tenant`
13
+ # after authentication and its locale: an unauthenticated request still
14
+ # gets its 401, the refusal is in the request's language, and a host base
15
+ # controller's own callbacks (which set the tenant) have run.
16
+ #
17
+ # @api public
18
+ module TenantRequired
19
+ REFUSAL_REASON = 'tenant_missing'.freeze
20
+ API_SOURCE = 'api'.freeze
21
+
22
+ private
23
+
24
+ def pf_require_tenant
25
+ return unless pf_tenant_missing?
26
+
27
+ api = self.class::AUDIT_SOURCE == API_SOURCE
28
+ pf_audit_refused(api ? 'api.auth_failed' : 'admin.auth_failed', data: { 'reason' => REFUSAL_REASON })
29
+ if api
30
+ render json: { error: { status: 403, messages: I18n.t('permission_forge.api.errors.tenant_required') } },
31
+ status: :forbidden
32
+ else
33
+ render plain: I18n.t('permission_forge.errors.tenant_required'), status: :forbidden
34
+ end
35
+ end
36
+
37
+ def pf_tenant_missing?
38
+ config = PermissionForge.configuration
39
+ config.tenant_enabled? && !config.admin_tenantless_access_allowed? && PermissionForge.ambient_tenant.nil?
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Api
5
+ # Read-only view of the audit log. Gated by api_enabled and api_auth (the
6
+ # base controller) and then by config.audit_log_authorization
7
+ # (Audit::LogAccess). Rows are tenant-scoped by the model.
8
+ class AuditEventsController < BaseController
9
+ before_action :authorize_audit_log!
10
+
11
+ rescue_from PermissionForge::Audit::EventQuery::Invalid do |error|
12
+ render json: { error: { status: 422, messages: error.errors } }, status: :unprocessable_entity
13
+ end
14
+
15
+ def index
16
+ render_collection(PermissionForge::Audit::EventQuery.call(params), serializer_for(:audit_event))
17
+ end
18
+
19
+ def show
20
+ render_record(find_record(PermissionForge::AuditEvent), serializer_for(:audit_event))
21
+ end
22
+
23
+ private
24
+
25
+ def authorize_audit_log!
26
+ return if PermissionForge::Audit::LogAccess.allowed?(self)
27
+
28
+ pf_audit_refused('api.auth_failed', data: { 'reason' => 'audit_log_not_authorized' })
29
+ render_not_authorized
30
+ end
31
+ end
32
+ end
33
+ end