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
data/config/routes.rb ADDED
@@ -0,0 +1,143 @@
1
+ PermissionForge::Engine.routes.draw do
2
+ # Engine-served CSS/JS/brand files — no host asset pipeline required.
3
+ get 'pf-assets/:file', to: 'assets#show', as: :pf_asset, constraints: { file: /[\w.-]+/ }, format: false
4
+
5
+ root to: 'home#index'
6
+
7
+ # Per-browser admin preferences (theme, dismissed helper banners).
8
+ resource :preferences, only: :update
9
+
10
+ get 'settings', to: 'settings#show', as: :settings
11
+ get 'field_lookup', to: 'field_lookups#show', as: :field_lookup
12
+
13
+ # The audit log (read-only): the filtered log and its CSV / JSON export, one
14
+ # event by uuid, and the "who holds what" export.
15
+ resources :audit_events, path: 'audit', only: %i[index show] do
16
+ collection do
17
+ get :holdings
18
+ end
19
+ end
20
+
21
+ # Home "needs attention": remove expired assignments (dry run shown inline on Home).
22
+ delete 'maintenance/cleanup', to: 'maintenance#cleanup', as: :maintenance_cleanup
23
+
24
+ resources :people, only: %i[index show] do
25
+ member do
26
+ get :remove_all, action: :confirm_remove_all
27
+ delete :remove_all
28
+ post :refresh_cache
29
+ get :consent, action: :consent_preview
30
+ post :consent, action: :withdraw_consent
31
+ get :erasure, action: :erasure_preview
32
+ post :erasure, action: :erase
33
+ end
34
+ end
35
+
36
+ resources :assignments, only: %i[new create update destroy] do
37
+ member do
38
+ get :remove, action: :confirm_destroy
39
+ post :keep
40
+ end
41
+ collection do
42
+ post :restore
43
+ post :undo
44
+ end
45
+ end
46
+
47
+ resources :permission_sets, only: %i[index new create show destroy] do
48
+ member do
49
+ post :review
50
+ patch :apply
51
+ post :duplicate
52
+ get :delete, action: :confirm_destroy
53
+ get :switch_mode, action: :confirm_switch_mode
54
+ patch :switch_mode
55
+ patch :conditions
56
+ end
57
+
58
+ resource :bulk_assignment, only: %i[new create] do
59
+ post :preview
60
+ get :revoke, action: :confirm_revoke
61
+ delete :revoke
62
+ end
63
+
64
+ resources :resources, param: :model, only: %i[new create show destroy],
65
+ constraints: { model: %r{[^/]+} } do
66
+ member do
67
+ post :review
68
+ patch :apply
69
+ post :sync
70
+ get :delete, action: :confirm_destroy
71
+ end
72
+
73
+ resources :fields, controller: 'resource_fields', param: :key, only: %i[show update],
74
+ constraints: { key: %r{[^/]+} } do
75
+ member do
76
+ post :acknowledge
77
+ post :apply_recommendation
78
+ end
79
+ end
80
+ resources :actions, controller: 'resource_actions', only: %i[new create show update destroy] do
81
+ member do
82
+ get :delete, action: :confirm_destroy
83
+ end
84
+ end
85
+ resources :rules, controller: 'permission_set_rules', only: %i[new create edit update destroy] do
86
+ collection do
87
+ post :refresh
88
+ end
89
+ member do
90
+ patch :toggle
91
+ get :delete, action: :confirm_destroy
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ resources :groups, controller: 'permission_set_groups', only: %i[index create show update destroy] do
98
+ member do
99
+ post :duplicate
100
+ get :delete, action: :confirm_destroy
101
+ post :add_set
102
+ delete :remove_set
103
+ end
104
+ resources :members, controller: 'permission_set_group_users', only: %i[create destroy]
105
+ end
106
+
107
+ # JSON Admin API — routes are always drawn; access is guarded at runtime
108
+ # by BaseController checking api_enabled + api_auth configuration.
109
+ namespace :api, defaults: { format: :json } do
110
+ resource :stats, only: [:show], controller: 'dashboard'
111
+
112
+ # The audit log is read-only over the API: no write route exists.
113
+ resources :audit_events, only: %i[index show], param: :identifier
114
+
115
+ resources :permission_sets, param: :identifier do
116
+ member do
117
+ post :clone
118
+ end
119
+
120
+ resources :resources, param: :identifier do
121
+ member do
122
+ post :generate_fields
123
+ end
124
+ resources :fields, param: :identifier, controller: 'resource_fields'
125
+ resources :actions, param: :identifier, controller: 'resource_actions'
126
+ resources :rules, param: :identifier, controller: 'permission_set_rules'
127
+ end
128
+
129
+ resources :users, param: :identifier, controller: 'permission_set_users', only: %i[index create destroy] do
130
+ collection do
131
+ post :bulk
132
+ end
133
+ end
134
+ end
135
+
136
+ resources :permission_set_groups, param: :identifier do
137
+ member do
138
+ post :clone
139
+ end
140
+ resources :users, param: :identifier, controller: 'permission_set_group_users', only: %i[index create destroy]
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,105 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/active_record'
3
+
4
+ module PermissionForge
5
+ module Generators
6
+ class InstallGenerator < Rails::Generators::Base
7
+ include ActiveRecord::Generators::Migration
8
+
9
+ source_root File.expand_path('templates', __dir__)
10
+
11
+ class_option :skip_migration, type: :boolean, default: false,
12
+ desc: 'Skip migration generation (for existing tables)'
13
+ class_option :table_prefix, type: :string, default: 'permission_forge_',
14
+ desc: 'Table name prefix for all permission_forge tables'
15
+ class_option :tenant, type: :string, default: nil,
16
+ desc: 'Tenant model name for acts_as_tenant (e.g., Account)'
17
+ class_option :user_id_type, type: :string, default: nil,
18
+ desc: 'Column type for user ids: bigint, integer, uuid or string ' \
19
+ '(default: the primary key type of the configured user model)'
20
+ class_option :tenant_id_type, type: :string, default: nil,
21
+ desc: 'Column type for the tenant column ' \
22
+ '(default: the primary key type of the --tenant model)'
23
+
24
+ KEY_TYPES = %w[bigint integer uuid string].freeze
25
+ DETECTED_KEY_TYPES = { integer: 'bigint', uuid: 'uuid', string: 'string' }.freeze
26
+
27
+ def copy_initializer
28
+ template 'initializer.rb.tt', 'config/initializers/permission_forge.rb'
29
+ end
30
+
31
+ def copy_migration
32
+ return if options[:skip_migration]
33
+
34
+ migration_template 'migration.rb.tt', 'db/migrate/create_permission_forge_tables.rb'
35
+ end
36
+
37
+ private
38
+
39
+ def table_prefix
40
+ options[:table_prefix]
41
+ end
42
+
43
+ def tenant_model
44
+ options[:tenant].presence
45
+ end
46
+
47
+ # 'Account' and 'Billing::Account' both map to account_id — the column
48
+ # name cannot carry a namespace.
49
+ def tenant_column
50
+ return nil unless tenant_model
51
+
52
+ "#{tenant_model.demodulize.underscore}_id"
53
+ end
54
+
55
+ # Columns that store a host primary key (user ids, audit user ids, the
56
+ # tenant id) take the type of that key. A bigint column silently
57
+ # truncates a UUID to its leading digits, which makes different users
58
+ # (or tenants) share permission data.
59
+ def user_id_type
60
+ @user_id_type ||= key_type(options[:user_id_type], PermissionForge.configuration.user_class, '--user-id-type')
61
+ end
62
+
63
+ def tenant_id_type
64
+ return 'bigint' unless tenant_model
65
+
66
+ @tenant_id_type ||= key_type(options[:tenant_id_type], tenant_model, '--tenant-id-type')
67
+ end
68
+
69
+ def key_type(explicit, model_name, flag)
70
+ return validated_key_type(explicit, flag) if explicit.present?
71
+
72
+ detected_key_type(model_name) || assume_bigint(model_name, flag)
73
+ end
74
+
75
+ def validated_key_type(type, flag)
76
+ return type if KEY_TYPES.include?(type)
77
+
78
+ raise Thor::Error, "#{flag} must be one of #{KEY_TYPES.join(', ')} (got #{type.inspect})"
79
+ end
80
+
81
+ # nil when the model is not loadable, has no table yet, or has a
82
+ # composite primary key.
83
+ def detected_key_type(model_name)
84
+ model = model_name.to_s.safe_constantize
85
+ return unless model.respond_to?(:columns_hash)
86
+
87
+ primary_key = model.primary_key
88
+ column = model.columns_hash[primary_key] if primary_key.is_a?(String)
89
+ DETECTED_KEY_TYPES[column&.type]
90
+ rescue ActiveRecord::ActiveRecordError
91
+ nil
92
+ end
93
+
94
+ def assume_bigint(model_name, flag)
95
+ say_status :warning, "could not read the primary key type of #{model_name}; assuming bigint. " \
96
+ "Pass #{flag}=uuid (or string) if its keys are not integers.", :yellow
97
+ 'bigint'
98
+ end
99
+
100
+ def migration_version
101
+ "[#{ActiveRecord::Migration.current_version}]"
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,148 @@
1
+ # PermissionForge — the options you are most likely to set. Every option, its
2
+ # default and what it does:
3
+ # https://github.com/fetijashari/permission_forge/blob/main/docs/configuration.md
4
+ # Before production, go through the Production hardening checklist
5
+ # (https://github.com/fetijashari/permission_forge#production-hardening-checklist);
6
+ # `bin/rails permission_forge:audit` reports what is still open.
7
+ PermissionForge.configure do |config|
8
+ # ---------------------------------------------------------------------------
9
+ # Closed for new installs (the library defaults stay open for existing apps)
10
+ # ---------------------------------------------------------------------------
11
+ # A guarded controller action (ControllerActionGuard) with nobody signed in is refused.
12
+ config.action_guard_nil_user_strategy = :deny
13
+ # Conditional Permission Sets fail closed without a request context (see below).
14
+ config.conditional_sets_strict_mode = true
15
+ # The cache is Rails.cache: use a shared store in production (Redis, Solid Cache, Memcached).
16
+ config.cache_backend = :rails_cache
17
+
18
+ # config.preset = :strict closes every fail-open default at once
19
+ # (docs/configuration.md#presets; a setting you make here still wins):
20
+ # nil_user_strategy = :deny, auto_query_scoping = :filter_only,
21
+ # response_filter_nil_user_strategy = :block, unconfigured_action_strategy = :block,
22
+ # mask_virtual_attributes = :auto, extra_fields_default_access = :unconfigured,
23
+ # and the three settings above.
24
+ # What it costs: with nil_user_strategy :deny and auto_query_scoping :filter_only,
25
+ # code that runs with no current user (jobs, mailers, seeds, webhooks, the
26
+ # console, sign-in lookups on a user model that includes PermissionScopedFindable)
27
+ # sees no protected rows, and a belongs_to presence check on a protected parent
28
+ # fails ("must exist"). Run such code with a user, or inside
29
+ # PermissionForge.without_permission_scope { }, where nobody signed in gets no rules:
30
+ # never around code an anonymous request reaches. Never include PermissionScopedFindable in the user model.
31
+ # config.preset = :strict
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # Who is signed in
35
+ # ---------------------------------------------------------------------------
36
+ # The class name of your User model (string, lazily constantized), and the
37
+ # class that created_by / updated_by point to.
38
+ config.user_class = 'User'
39
+ config.audit_user_class = 'User'
40
+
41
+ # Returns the signed-in user, or nil. As generated it reads Current.user when
42
+ # the app has a Current class that answers `user` (Rails 8's authentication
43
+ # generator creates one) and nil otherwise: until you wire it, nobody is
44
+ # signed in.
45
+ config.current_user_resolver = -> { ::Current.user if defined?(::Current) && ::Current.respond_to?(:user) }
46
+
47
+ # Whether a user is active. It does not remove access: the audit task
48
+ # reports the assignments still held by users it calls inactive.
49
+ config.user_active_check = ->(user) { user.respond_to?(:active_for_authentication?) ? user.active_for_authentication? : true }
50
+
51
+ # The profile cached with each user's permissions (uuid: nil when users have none).
52
+ config.profile_builder = ->(user) {
53
+ { id: user.id, uuid: user.try(:uuid), name: user.try(:name), email: user.try(:email) }
54
+ }
55
+
56
+ # Prefix of the PermissionForge tables ('' for none).
57
+ config.table_name_prefix = '<%= table_prefix %>'
58
+
59
+ # ---------------------------------------------------------------------------
60
+ # Multi-tenancy (optional, requires the acts_as_tenant gem)
61
+ # ---------------------------------------------------------------------------
62
+ # Every PermissionForge table needs the tenant column (the install migration
63
+ # adds it with --tenant=Account). Boot fails if this is set while
64
+ # acts_as_tenant is not loaded.
65
+ <% if tenant_model %> config.tenant_model = '<%= tenant_model %>'
66
+ <% else %> # config.tenant_model = 'Account'
67
+ <% end %>
68
+ # The admin screens and the admin API serve one tenant at a time: a request
69
+ # with no current tenant is refused (403). Set the tenant in
70
+ # base_controller_class / api_base_controller_class (or in middleware).
71
+ # config.admin_tenantless_access = :deny
72
+
73
+ # ---------------------------------------------------------------------------
74
+ # Activation conditions (session flag / allowed IPs / login hours)
75
+ # ---------------------------------------------------------------------------
76
+ # With conditional_sets_strict_mode (set above), where the request context is
77
+ # blank (no resolver, a job, the console) a conditional GRANTING set does not
78
+ # apply and a conditional MUTING set (a lockdown) does. Trade-off: an MFA- or
79
+ # IP-gated grant never applies in jobs or the console, nor in requests until
80
+ # this resolver returns the request's IP and session flags:
81
+ # config.request_context_resolver = -> {
82
+ # { ip: Current.ip, session_flags: Current.session_flags, time: Time.current }
83
+ # }
84
+ # It runs on every permission read of a user who holds a conditional set:
85
+ # keep it cheap, never read permissions inside it, return {} with no request.
86
+
87
+ # ---------------------------------------------------------------------------
88
+ # Masked attributes with nobody signed in (not part of the preset)
89
+ # ---------------------------------------------------------------------------
90
+ # :bypass (default) returns the stored values; :hide hides every guarded
91
+ # attribute. Trade-off: jobs, mailers and console code that read masked
92
+ # models with no user then see hidden values: set the current user there.
93
+ # The user model (user_class and its subclasses) stays readable for sign-in,
94
+ # and validations always read the stored values.
95
+ # config.masked_attributes_nil_user_strategy = :hide
96
+
97
+ # ---------------------------------------------------------------------------
98
+ # Admin screens
99
+ # ---------------------------------------------------------------------------
100
+ # HTTP Basic auth is on and answers 401 until credentials are set. Never turn
101
+ # it off without the second option: the admin can grant anything to anyone.
102
+ #
103
+ # Option A — Basic auth credentials (from ENV or credentials, never literals):
104
+ # config.http_basic_auth_user = ENV['PF_ADMIN_USER']
105
+ # config.http_basic_auth_password = ENV['PF_ADMIN_PASSWORD']
106
+ #
107
+ # Option B — your own authenticated admin controller:
108
+ # config.http_basic_auth_enabled = false
109
+ # config.base_controller_class = '::Admin::BaseController' # must authenticate admins
110
+ #
111
+ # Who may grant, revoke, extend or review an assignment, or change a group's
112
+ # members (admin screens and API). Example: no self-assignment.
113
+ # config.admin_assignment_authorization = ->(controller, target, user_ids, action) {
114
+ # admin = controller.current_user
115
+ # admin&.admin? && !user_ids.include?(admin.id)
116
+ # }
117
+ #
118
+ # Who may read the audit log pages, exports and API (nil: anyone with admin access).
119
+ # config.audit_log_authorization = ->(controller) { controller.current_user&.auditor? }
120
+
121
+ # ---------------------------------------------------------------------------
122
+ # Audit log (docs/audit-log.md)
123
+ # ---------------------------------------------------------------------------
124
+ # Every permission change is recorded in the audit_events table, inside the
125
+ # change's own transaction. Each option with its default (an example where that is nil):
126
+ # config.audit_events = :auto # true: always; false: never (events are still published)
127
+ # config.audit_actor_resolver = ->(controller) { controller.current_user } # nil: current_user_resolver
128
+ # config.audit_request_context = ->(controller) { { request_id: controller.request.request_id, ip: controller.request.remote_ip } }
129
+ # config.audit_change_failure_strategy = :raise # the change rolls back; or :report, :log
130
+ # config.audit_hook_failure_strategy = :report # or :log, :raise
131
+ # config.audit_runtime_events = %i[denied] # also: bypass field_read params_filtered cache
132
+ # config.audit_log_subscriber = false # true: also write every event to Rails.logger
133
+ # config.audit_retention_period = 2.years # nil keeps events forever; purge: bin/rails permission_forge:audit_events:purge
134
+ # In jobs and rake tasks: PermissionForge.with_audit_actor(user: admin, source: 'job') { ... }
135
+
136
+ # ---------------------------------------------------------------------------
137
+ # JSON admin API (off by default)
138
+ # ---------------------------------------------------------------------------
139
+ # config.api_enabled = true
140
+ # config.api_base_controller_class = '::Api::BaseController' # your auth chain
141
+ # config.api_auth = ->(controller) { controller.current_user&.admin? }
142
+ # config.api_action_authorization = ->(controller, action, record) { controller.current_user&.admin? }
143
+ # config.api_assignment_authorization = ->(controller, permission_set, user_ids) { controller.current_user&.admin? }
144
+ #
145
+ # api_auth proves identity only: until both authorization lambdas are set,
146
+ # every API change and assignment is refused (403).
147
+ # config.api_require_authorization = true # default
148
+ end
@@ -0,0 +1,198 @@
1
+ class CreatePermissionForgeTables < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ create_table :<%= table_prefix %>permission_set_groups do |t|
4
+ t.string :uuid, null: false
5
+ t.string :name, null: false, default: 'Untitled Permission Set Group'
6
+ t.string :description
7
+ t.boolean :is_active, null: false, default: true
8
+ t.boolean :is_editable, null: false, default: true
9
+ <% if tenant_column %> t.<%= tenant_id_type %> :<%= tenant_column %>
10
+ t.index :<%= tenant_column %>
11
+ <% end %> t.<%= user_id_type %> :created_by_id
12
+ t.<%= user_id_type %> :updated_by_id
13
+ t.timestamps
14
+
15
+ t.index :uuid, unique: true
16
+ end
17
+
18
+ create_table :<%= table_prefix %>permission_sets do |t|
19
+ t.string :uuid, null: false
20
+ t.references :permission_set_group, foreign_key: { to_table: :<%= table_prefix %>permission_set_groups }, null: true
21
+ t.string :name, null: false, default: 'Untitled Permission Set'
22
+ t.string :description
23
+ t.boolean :is_active, null: false, default: true
24
+ t.boolean :is_editable, null: false, default: true
25
+ t.boolean :is_muting, null: false, default: false
26
+ t.string :requires_session_flag
27
+ t.string :allowed_ips
28
+ t.integer :allowed_hours_start
29
+ t.integer :allowed_hours_end
30
+ <% if tenant_column %> t.<%= tenant_id_type %> :<%= tenant_column %>
31
+ t.index :<%= tenant_column %>
32
+ <% end %> t.<%= user_id_type %> :created_by_id
33
+ t.<%= user_id_type %> :updated_by_id
34
+ t.timestamps
35
+
36
+ t.index :uuid, unique: true
37
+ end
38
+
39
+ create_table :<%= table_prefix %>resources do |t|
40
+ t.string :uuid, null: false
41
+ t.references :permission_set, foreign_key: { to_table: :<%= table_prefix %>permission_sets }, null: false
42
+ t.string :name, null: false, default: 'Untitled Resource'
43
+ t.string :label
44
+ t.string :description
45
+ t.boolean :is_active, null: false, default: true
46
+ t.boolean :is_editable, null: false, default: true
47
+ t.boolean :read_access, null: false, default: false
48
+ t.boolean :create_access, null: false, default: false
49
+ t.boolean :edit_access, null: false, default: false
50
+ t.boolean :delete_access, null: false, default: false
51
+ t.boolean :view_all, null: false, default: false
52
+ t.boolean :modify_all, null: false, default: false
53
+ <% if tenant_column %> t.<%= tenant_id_type %> :<%= tenant_column %>
54
+ t.index :<%= tenant_column %>
55
+ <% end %> t.<%= user_id_type %> :created_by_id
56
+ t.<%= user_id_type %> :updated_by_id
57
+ t.timestamps
58
+
59
+ t.index :uuid, unique: true
60
+ t.index [:permission_set_id, :name], unique: true, name: 'idx_pf_resources_unique'
61
+ end
62
+
63
+ create_table :<%= table_prefix %>resource_fields do |t|
64
+ t.string :uuid, null: false
65
+ t.references :resource, foreign_key: { to_table: :<%= table_prefix %>resources }, null: false
66
+ t.string :name, null: false, default: 'untitled_field'
67
+ t.string :label, null: false, default: 'Untitled Field'
68
+ t.string :description
69
+ t.boolean :is_active, null: false, default: true
70
+ t.boolean :is_editable, null: false, default: true
71
+ t.boolean :read_access, null: false, default: false
72
+ t.boolean :edit_access, null: false, default: false
73
+ t.boolean :mask_access, null: false, default: false
74
+ t.string :mask_strategy, null: false, default: 'full'
75
+ t.datetime :sensitive_acknowledged_at
76
+ t.<%= user_id_type %> :sensitive_acknowledged_by_id
77
+ <% if tenant_column %> t.<%= tenant_id_type %> :<%= tenant_column %>
78
+ t.index :<%= tenant_column %>
79
+ <% end %> t.<%= user_id_type %> :created_by_id
80
+ t.<%= user_id_type %> :updated_by_id
81
+ t.timestamps
82
+
83
+ t.index :uuid, unique: true
84
+ t.index [:resource_id, :name], unique: true, name: 'idx_pf_resource_fields_unique'
85
+ end
86
+
87
+ create_table :<%= table_prefix %>resource_actions do |t|
88
+ t.string :uuid, null: false
89
+ t.references :resource, foreign_key: { to_table: :<%= table_prefix %>resources }, null: false
90
+ t.string :name, null: false
91
+ t.string :label
92
+ t.string :description
93
+ t.boolean :is_permitted, null: false, default: false
94
+ t.boolean :is_active, null: false, default: true
95
+ <% if tenant_column %> t.<%= tenant_id_type %> :<%= tenant_column %>
96
+ t.index :<%= tenant_column %>
97
+ <% end %> t.<%= user_id_type %> :created_by_id
98
+ t.<%= user_id_type %> :updated_by_id
99
+ t.timestamps
100
+
101
+ t.index :uuid, unique: true
102
+ t.index [:resource_id, :name], unique: true, name: 'idx_pf_resource_actions_unique'
103
+ end
104
+
105
+ create_table :<%= table_prefix %>permission_set_rules do |t|
106
+ t.string :uuid, null: false
107
+ t.references :permission_set, foreign_key: { to_table: :<%= table_prefix %>permission_sets }, null: false
108
+ t.references :resource, foreign_key: { to_table: :<%= table_prefix %>resources }, null: false
109
+ t.string :name, null: false, default: 'Untitled Rule'
110
+ t.string :failure_message, default: 'You do not have permission to perform this action.'
111
+ t.string :join_type
112
+ t.string :field_name, null: false
113
+ t.string :field_value, null: false
114
+ t.string :field_accessor_path
115
+ t.string :operator, null: false, default: 'equal'
116
+ t.boolean :include_nulls, null: false, default: true
117
+ t.boolean :is_active, null: false, default: true
118
+ t.boolean :is_editable, null: false, default: true
119
+ <% if tenant_column %> t.<%= tenant_id_type %> :<%= tenant_column %>
120
+ t.index :<%= tenant_column %>
121
+ <% end %> t.<%= user_id_type %> :created_by_id
122
+ t.<%= user_id_type %> :updated_by_id
123
+ t.timestamps
124
+
125
+ t.index :uuid, unique: true
126
+ end
127
+
128
+ create_table :<%= table_prefix %>permission_set_users do |t|
129
+ t.string :uuid, null: false
130
+ t.references :permission_set, foreign_key: { to_table: :<%= table_prefix %>permission_sets }, null: false
131
+ t.references :user, null: false, type: :<%= user_id_type %>
132
+ t.datetime :expires_at
133
+ t.datetime :reviewed_at
134
+ t.<%= user_id_type %> :reviewed_by_id
135
+ t.string :reason
136
+ <% if tenant_column %> t.<%= tenant_id_type %> :<%= tenant_column %>
137
+ t.index :<%= tenant_column %>
138
+ <% end %> t.<%= user_id_type %> :created_by_id
139
+ t.<%= user_id_type %> :updated_by_id
140
+ t.timestamps
141
+
142
+ t.index :uuid, unique: true
143
+ t.index [:permission_set_id, :user_id], unique: true, name: 'idx_pf_pset_users_unique'
144
+ end
145
+
146
+ create_table :<%= table_prefix %>permission_set_group_users do |t|
147
+ t.string :uuid, null: false
148
+ t.references :permission_set_group, foreign_key: { to_table: :<%= table_prefix %>permission_set_groups }, null: false
149
+ t.references :user, null: false, type: :<%= user_id_type %>
150
+ t.string :reason
151
+ <% if tenant_column %> t.<%= tenant_id_type %> :<%= tenant_column %>
152
+ t.index :<%= tenant_column %>
153
+ <% end %> t.<%= user_id_type %> :created_by_id
154
+ t.<%= user_id_type %> :updated_by_id
155
+ t.timestamps
156
+
157
+ t.index :uuid, unique: true
158
+ t.index [:permission_set_group_id, :user_id], unique: true, name: 'idx_pf_pset_group_users_unique'
159
+ end
160
+
161
+ # Insert-only audit log. No foreign keys: events outlive the records they describe.
162
+ create_table :<%= table_prefix %>audit_events do |t|
163
+ t.string :uuid, null: false
164
+ t.datetime :occurred_at, null: false, precision: 6
165
+ t.string :event, null: false
166
+ t.string :category, null: false
167
+ t.string :actor_type, null: false
168
+ t.string :actor_id
169
+ t.string :actor_label
170
+ t.string :source, null: false
171
+ t.string :record_type
172
+ t.string :record_id
173
+ t.string :record_uuid
174
+ t.string :record_label
175
+ t.bigint :permission_set_id
176
+ t.string :permission_set_uuid
177
+ t.string :subject_user_id
178
+ t.text :data
179
+ t.string :reason
180
+ t.string :request_id
181
+ t.string :ip
182
+ t.string :correlation_id
183
+ <% if tenant_column %> t.<%= tenant_id_type %> :<%= tenant_column %>
184
+ t.index :<%= tenant_column %>, name: 'idx_pf_audit_tenant'
185
+ <% end %> t.datetime :created_at, null: false
186
+
187
+ t.index :uuid, unique: true, name: 'idx_pf_audit_uuid'
188
+ t.index :occurred_at, name: 'idx_pf_audit_occurred'
189
+ t.index [:event, :occurred_at], name: 'idx_pf_audit_event'
190
+ t.index [:subject_user_id, :occurred_at], name: 'idx_pf_audit_subject'
191
+ t.index [:permission_set_id, :occurred_at], name: 'idx_pf_audit_pset'
192
+ t.index [:permission_set_uuid, :occurred_at], name: 'idx_pf_audit_pset_uuid'
193
+ t.index [:record_type, :record_id, :occurred_at], name: 'idx_pf_audit_record'
194
+ t.index [:actor_id, :occurred_at], name: 'idx_pf_audit_actor'
195
+ t.index :correlation_id, name: 'idx_pf_audit_correlation'
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,57 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/active_record'
3
+
4
+ module PermissionForge
5
+ # @api private
6
+ module Generators
7
+ # Installs Postgres Row-Level Security for PermissionForge (Level 3).
8
+ #
9
+ # bin/rails generate permission_forge:rls
10
+ #
11
+ # Creates a migration that enables (and forces) RLS on every protected
12
+ # table and installs policies generated from the current rule state via
13
+ # PermissionForge::RLS.sync!. After editing rules, re-sync with
14
+ # `rake permission_forge:rls:sync` (drift is warned about at boot).
15
+ #
16
+ # Requires PostgreSQL and config.database_row_level_security = true for the
17
+ # runtime session-variable plumbing.
18
+ class RlsGenerator < Rails::Generators::Base
19
+ include ActiveRecord::Generators::Migration
20
+
21
+ source_root File.expand_path('templates', __dir__)
22
+
23
+ def check_adapter
24
+ return if PermissionForge::RLS.postgres?
25
+
26
+ say_status :error, 'PermissionForge RLS requires PostgreSQL — aborting.', :red
27
+ raise Thor::Error, 'database adapter is not PostgreSQL'
28
+ end
29
+
30
+ def copy_migration
31
+ migration_template 'enable_permission_forge_rls.rb.tt',
32
+ 'db/migrate/enable_permission_forge_rls.rb'
33
+ end
34
+
35
+ def show_post_install
36
+ say <<~MSG
37
+
38
+ RLS migration created. Next steps:
39
+ 1. bin/rails db:migrate
40
+ 2. Set config.database_row_level_security = true in the initializer
41
+ 3. Wrap request/job execution: PermissionForge::RLS.with_user(user) { ... }
42
+ (or set_user!/clear_user! in an around_action)
43
+ 4. After editing permission rules: rake permission_forge:rls:sync
44
+
45
+ IMPORTANT: connect as a NON-superuser role without BYPASSRLS —
46
+ superusers bypass row-level security entirely.
47
+ MSG
48
+ end
49
+
50
+ private
51
+
52
+ def migration_version
53
+ "[#{ActiveRecord::Migration.current_version}]"
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,15 @@
1
+ # Enables Postgres Row-Level Security on all PermissionForge-protected tables
2
+ # and installs policies generated from the current rule state.
3
+ #
4
+ # Policies check set membership dynamically (assign/revoke needs NO resync) but
5
+ # bake rule content statically — after editing permission rules, run:
6
+ # rake permission_forge:rls:sync
7
+ class EnablePermissionForgeRls < ActiveRecord::Migration<%= migration_version %>
8
+ def up
9
+ PermissionForge::RLS.sync!
10
+ end
11
+
12
+ def down
13
+ PermissionForge::RLS.remove!
14
+ end
15
+ end
@@ -0,0 +1,8 @@
1
+ class PermissionForgeAddAdminUiColumns < ActiveRecord::Migration<%= migration_version %>
2
+ # Nullable and additive: safe to run on a live table. The admin UI checks for
3
+ # the column and simply hides "This is intended" until it exists.
4
+ def change
5
+ add_column :<%= table_prefix %>resource_fields, :sensitive_acknowledged_at, :datetime,
6
+ if_not_exists: true
7
+ end
8
+ end