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/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Feti Jashari and Confidee
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,356 @@
1
+ <p align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/fetijashari/permission_forge/main/docs/assets/logo-horizontal-dark.svg">
4
+ <img alt="PermissionForge" src="https://raw.githubusercontent.com/fetijashari/permission_forge/main/docs/assets/logo-horizontal-light.svg" width="420">
5
+ </picture>
6
+ </p>
7
+
8
+ <p align="center">
9
+ <a href="https://github.com/fetijashari/permission_forge/actions/workflows/ci.yml"><img src="https://github.com/fetijashari/permission_forge/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
10
+ <a href="https://rubygems.org/gems/permission_forge"><img src="https://img.shields.io/gem/v/permission_forge" alt="Gem Version"></a>
11
+ <a href="https://www.ruby-lang.org"><img src="https://img.shields.io/badge/ruby-%3E%3D%203.2-ruby.svg" alt="Ruby"></a>
12
+ <a href="https://rubyonrails.org"><img src="https://img.shields.io/badge/rails-7.1%20to%208.1-red.svg" alt="Rails"></a>
13
+ <a href="https://github.com/fetijashari/permission_forge/blob/main/LICENSE.txt"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT"></a>
14
+ </p>
15
+
16
+ <p align="center">
17
+ Database-backed permissions for Ruby on Rails.<br>
18
+ Resource CRUD and custom actions, field-level read/edit access with masking, and row-level query rules, edited at runtime and cached per user.
19
+ </p>
20
+
21
+ ---
22
+
23
+ ## Overview
24
+
25
+ PermissionForge implements a **three-tier permission model**:
26
+
27
+ | Tier | Scope | Controls |
28
+ |------|-------|----------|
29
+ | **Resource** | Model | Can this user read/create/edit/delete Customers? Run the `export` action? |
30
+ | **Field** | Column | Can this user see `email`? Masked or in full? Edit `name`? |
31
+ | **Rule** | Row | Only records where `owner_id = current_user.id` |
32
+
33
+ Permissions live in your database as **Permission Sets** (collections of resources, fields, actions and rules) that are
34
+ assigned to users, optionally grouped, and edited in the built-in admin while the app runs. Each user's merged
35
+ permissions are cached as one hash.
36
+
37
+ ### Features
38
+
39
+ - **Resource-level CRUD and custom actions** per model (`can_read?`, `can?(:export, :Customer)`), and an optional
40
+ controller guard that checks them before an action runs ([Controller Actions](https://github.com/fetijashari/permission_forge/blob/main/docs/controller-actions.md))
41
+ - **Field-level read/edit access**, enforced on the model's attribute readers and serialization (`MaskedAttributes`), on
42
+ incoming params (`PermitsAttributes`) and on response hashes (`FiltersResponseAttributes`)
43
+ - **Field masking** with four built-in strategies (`full`, `email`, `last4`, `first1_last4`) and your own
44
+ - **Row-level query rules**: 15 operators, 3 join types, values compared in the column's type on PostgreSQL, MySQL and
45
+ SQLite; applied by `Model.find_by` / `find_by!` and `Model.permission_scoped`
46
+ - **Optional query scoping**: a `default_scope` for every query, detection of queries that skipped the rules, and
47
+ PostgreSQL Row-Level Security policies ([Query Scoping](https://github.com/fetijashari/permission_forge/blob/main/docs/query-scoping.md))
48
+ - **Muting Permission Sets** that subtract access, **time-bound assignments**, and **activation conditions** (session
49
+ flag, IP allowlist, login hours), evaluated for each request
50
+ - **Audit log** of every permission change made through the models, written in the change's transaction
51
+ - **Admin UI** with no host asset dependencies, and an **opt-in JSON admin API**
52
+ - **Personal-data helpers**: masks suggested for personal-data columns when fields are generated, a field auditor, and
53
+ log parameter filtering for those columns (on by default)
54
+ - **Any Rails cache store** (`Rails.cache` over Redis, Solid Cache, Memcached, file; or in-memory for tests)
55
+ - **Multi-tenancy** through `acts_as_tenant`, and three locales (English, Albanian, Macedonian)
56
+ - **No hard dependencies** on Devise, Pundit, Rolify or any auth framework: every integration point is a lambda
57
+
58
+ ### How it differs from Pundit, CanCanCan and Action Policy
59
+
60
+ Those are policy libraries: you write rules in Ruby and call them where you decide. PermissionForge is a permissions
61
+ platform: rules are data that admins edit at runtime, and the gem hooks into your app to apply them. It overrides
62
+ `find_by` on models you protect, wraps attribute readers and serialization, can add a `default_scope`, runs checks at
63
+ boot, and mounts an admin. That reaches further than a policy library and couples more tightly to your models; it can
64
+ sit next to one (for example Pundit for controller decisions, PermissionForge for fields and rows).
65
+
66
+ ---
67
+
68
+ ## Installation
69
+
70
+ Add to your `Gemfile`:
71
+
72
+ ```ruby
73
+ gem 'permission_forge'
74
+ ```
75
+
76
+ Then run:
77
+
78
+ ```bash
79
+ bundle install
80
+ bin/rails generate permission_forge:install
81
+ bin/rails db:migrate
82
+ ```
83
+
84
+ This creates `config/initializers/permission_forge.rb` and a migration with 9 tables: the 8 permission tables and the
85
+ audit log, prefixed with `permission_forge_` by default.
86
+
87
+ Generator options:
88
+ - `--skip-migration`: no migration (for apps with existing tables)
89
+ - `--table-prefix=PREFIX`: table name prefix (default `permission_forge_`; `''` for none)
90
+ - `--user-id-type=TYPE`: type of the user id columns, `bigint`, `integer`, `uuid` or `string` (default: detected from
91
+ your user model's primary key, so run the generator after your users table exists)
92
+ - `--tenant=MODEL`: add a tenant column for [multi-tenancy](https://github.com/fetijashari/permission_forge/blob/main/docs/multi-tenancy.md)
93
+ (`--tenant-id-type=TYPE` to override its detected type)
94
+
95
+ Requirements: Ruby 3.2+, Rails 7.1 to 8.1, PostgreSQL, MySQL or SQLite.
96
+
97
+ Upgrading? Read the [CHANGELOG](https://github.com/fetijashari/permission_forge/blob/main/CHANGELOG.md) and the
98
+ [migration guide](https://github.com/fetijashari/permission_forge/blob/main/docs/migration-guide.md).
99
+
100
+ ---
101
+
102
+ ## Quick Start
103
+
104
+ ### 1. Tell it who is signed in
105
+
106
+ The generated initializer reads `Current.user` when your app has a `Current` class that answers `user` (as Rails 8's
107
+ authentication generator creates). Otherwise point it at your own lookup:
108
+
109
+ ```ruby
110
+ # config/initializers/permission_forge.rb
111
+ PermissionForge.configure do |config|
112
+ config.user_class = 'User'
113
+ # As generated; or however your app knows the user
114
+ config.current_user_resolver = -> { ::Current.user if defined?(::Current) && ::Current.respond_to?(:user) }
115
+ config.cache_backend = :rails_cache
116
+ end
117
+ ```
118
+
119
+ Until it returns a user, nobody is signed in and `nil_user_strategy` decides (see the checklist below).
120
+
121
+ Configure PermissionForge in `config/initializers` only. The gem's models (`PermissionForge::PermissionSet`, ...) are
122
+ autoloaded by your application right after the initializers and read `user_class`, `audit_user_class` and
123
+ `tenant_model` as they load; a setting changed later (in `after_initialize`, a console) does not reach a loaded class
124
+ (PermissionForge logs a warning). Reference a model from a method (your models, controllers, jobs and rake tasks), not
125
+ from the top level of an initializer, an engine or railtie initializer, or a file required from
126
+ `config/application.rb`: the boot then fails with a `NameError` that says so. To change one of its classes (include a
127
+ module, add a callback), use `Rails.application.config.to_prepare` or
128
+ `Rails.autoloaders.main.on_load('PermissionForge::PermissionSet')`, which run again after every code reload;
129
+ `config.after_initialize` runs once, so use it only to read.
130
+
131
+ ### 2. Include the concern in your User model
132
+
133
+ ```ruby
134
+ class User < ApplicationRecord
135
+ include PermissionForge::Concerns::PermissionedUser
136
+ end
137
+ ```
138
+
139
+ Every private method PermissionForge adds to your User class starts with `pf_`, so a method of your own never
140
+ replaces one of the gem's. The public API is the documented one (`permissions_hash`, `regenerate_permissions`,
141
+ `invalidate_permissions`, `permission_cache_key`, `permission_invalidation_stamp`, `can?`, `can_*?`,
142
+ `permissions_for`, `permissions_hash_for`, `masked_field?`, `mask_strategy_for`, `field_permissions`); override
143
+ those, not the private helpers.
144
+
145
+ ### 3. Protect models with row-level rules (optional)
146
+
147
+ ```ruby
148
+ class Customer < ApplicationRecord
149
+ include PermissionForge::Concerns::PermissionScopedFindable
150
+ end
151
+ ```
152
+
153
+ This applies the user's rules to the class-level `Customer.find_by` / `find_by!` and adds a chainable
154
+ `Customer.permission_scoped` relation. Nothing else is scoped by default: `find`, `where`, `scope.find_by` and
155
+ association traversal return every row. Start from `permission_scoped`, or turn on one of the
156
+ [query-scoping levels](https://github.com/fetijashari/permission_forge/blob/main/docs/query-scoping.md). A user with no
157
+ rules for a model gets every row.
158
+
159
+ ### 4. Create a Permission Set and assign it
160
+
161
+ ```ruby
162
+ pset = PermissionForge::PermissionSet.create!(name: 'Sales Team', is_active: true)
163
+
164
+ resource = PermissionForge::Resource.create!(
165
+ permission_set: pset,
166
+ name: 'Customer',
167
+ read_access: true, create_access: true,
168
+ edit_access: true, delete_access: false
169
+ )
170
+
171
+ PermissionForge::ResourceField.create!(
172
+ resource: resource, name: 'name', label: 'Name',
173
+ read_access: true, edit_access: true
174
+ )
175
+
176
+ PermissionForge::PermissionSetUser.create!(permission_set: pset, user: user)
177
+ ```
178
+
179
+ Each change invalidates the cached permissions of the users it affects; the next read rebuilds them. The admin UI
180
+ (mounted as described in [Admin UI](https://github.com/fetijashari/permission_forge/blob/main/docs/admin-ui.md)) does
181
+ the same through forms.
182
+
183
+ ### 5. Check permissions at runtime
184
+
185
+ ```ruby
186
+ user.can_read?('Customer') # => true
187
+ user.can_delete?('Customer') # => false
188
+ user.permissions_for(:Customer)
189
+ # => { read_access: true, create_access: true, edit_access: true, delete_access: false,
190
+ # view_all: false, modify_all: false }
191
+ ```
192
+
193
+ Resource CRUD flags are answers to these calls. They do not by themselves stop a getter or `find_by`: enforce them
194
+ where you call `can_*?`, or with `ControllerActionGuard`. Field access is decided by the field rows.
195
+
196
+ ---
197
+
198
+ ## Production hardening checklist
199
+
200
+ Several defaults fail open so that installing the gem changes nothing until you configure it. Before production, go
201
+ through this list. `bin/rails permission_forge:audit` reports most of these settings.
202
+
203
+ `config.preset = :strict` closes every row below that a setting can close, except masking with nobody signed in (an
204
+ explicit choice: see its row); a setting you make yourself always wins over the preset. New installs get the safe
205
+ part (`action_guard_nil_user_strategy = :deny`, `conditional_sets_strict_mode = true`, `cache_backend = :rails_cache`)
206
+ and the preset as a commented line with what it costs. The rows that need your code (`base_controller_class`,
207
+ `admin_assignment_authorization`, `audit_log_authorization`, `user_active_check`) are not in a preset.
208
+ `bin/rails permission_forge:audit` prints the preset and, under `:strict`, every option set looser. See
209
+ [Presets](https://github.com/fetijashari/permission_forge/blob/main/docs/configuration.md#presets).
210
+
211
+ | Default | What it means | To close it |
212
+ |---|---|---|
213
+ | `nil_user_strategy = :bypass` | With no current user (jobs, console, a resolver that returns nil), `find_by`, `permission_scoped`, auto-scoping and the RLS policy apply no rules; so does the controller guard unless `action_guard_nil_user_strategy` is set | `:deny` (not found) or `:raise`. Do not include `PermissionScopedFindable` in the user model: under `:deny` sign-in lookups return nil (`permission_forge:audit` reports `user_model_scoped`) |
214
+ | `masked_attributes_nil_user_strategy = :bypass` | Attribute readers, `attributes` and `as_json` of a masked model return the stored values when nobody is signed in, whatever `nil_user_strategy` says | `:hide` (every guarded attribute reads as hidden with no user; the primary key and the raw paths are untouched), and set the current user in the jobs and mailers that read masked models. Not part of `config.preset = :strict`. The user model (`user_class` and its subclasses) stays readable for sign-in; validations read stored values |
215
+ | `response_filter_nil_user_strategy = :bypass` | `FiltersResponseAttributes` returns every attribute when there is no user | `:block` |
216
+ | `auto_query_scoping = :off` | Only class-level `find_by` / `find_by!` and `permission_scoped` apply row rules | `:filter_only`, and use `permission_scoped` for join rules; consider RLS on PostgreSQL |
217
+ | `unconfigured_action_strategy = :allow` | A controller action no Permission Set manages is allowed; unmapped actions are always allowed | `:block` |
218
+ | `action_guard_nil_user_strategy = nil` | The guard follows `nil_user_strategy` (`:bypass`) for signed-out requests to mapped actions | `:deny` (new installs get it from the generator) |
219
+ | `conditional_sets_strict_mode = false` | With an empty request context (no resolver configured), every conditional Permission Set applies, MFA- or IP-gated grants included | A `request_context_resolver` that returns the request's IP and session flags, and `true`: without a request context, conditional granting sets are then dropped and conditional muting sets still apply |
220
+ | `mask_virtual_attributes = false` | Virtual readers (methods, store accessors, attribute-API attributes) are not masked on the model; only `PermitsAttributes` and `FiltersResponseAttributes` apply their field rows | `:auto` (or a list of models), after checking that the models' own callbacks do not need the raw values |
221
+ | `extra_fields_default_access = :grant` | A declared virtual field no row configures gets the resource's read and edit access, unmasked | `:unconfigured` |
222
+ | `cache_backend = :memory` | Per process: invalidations and auto-applied masking do not reach other processes (boot warning in production) | `:rails_cache` over a shared store (Redis, Solid Cache, Memcached) |
223
+ | `http_basic_auth_enabled = true` | Without credentials the admin answers 401 (closed). With credentials it is one shared login with full admin power. With Basic auth off and `base_controller_class` left as `ActionController::Base`, the admin is open to anyone | Set `base_controller_class` to your authenticated admin controller, then turn Basic auth off |
224
+ | `admin_assignment_authorization = nil` | Every admin who reaches the admin can assign anyone to anything | A lambda that checks the acting admin |
225
+ | `audit_log_authorization = nil` | Every admin (and every API caller `api_auth` admits) can read and export the whole audit log | A lambda that returns exactly `true` for those allowed |
226
+ | `base_controller_class` / `api_base_controller_class` | With tenancy on, a request with no current tenant is refused (403) | Controllers that authenticate and set the tenant |
227
+ | `user_active_check` | Only reported by the security audit; an inactive user keeps every grant | Revoke the assignments of users you deactivate |
228
+
229
+ Upgrading from 0.13: once the last 0.13 process is gone, run `bin/rails permission_forge:cache:clear` (see the
230
+ [CHANGELOG](https://github.com/fetijashari/permission_forge/blob/main/CHANGELOG.md)).
231
+
232
+ Already closed by default, keep them so: `api_require_authorization = true` (API changes need
233
+ `api_action_authorization` and `api_assignment_authorization`), `admin_tenantless_access = :deny`,
234
+ `nested_attributes_parent_fallback = false`, `unconfigured_field_strategy = :block`, and the API is off
235
+ (`api_enabled = false`). If you use PostgreSQL RLS, run `bin/rails permission_forge:rls:sync` after each upgrade.
236
+
237
+ See [Configuration](https://github.com/fetijashari/permission_forge/blob/main/docs/configuration.md) for every option.
238
+
239
+ ## What is not intercepted
240
+
241
+ Masking and row rules work at the model layer. These paths read the database or the raw attributes directly:
242
+
243
+ - **Values:** `pluck`, `pick`, calculations (`minimum(:email)`, `maximum`, `sum`, `group(:email).count`),
244
+ `select("email AS contact")` and other SQL aliases (also through `find_by_sql`), `connection.select_*`,
245
+ `*_before_type_cast` / `read_attribute_before_type_cast` / `attributes_before_type_cast`, `*_in_database` /
246
+ `attribute_in_database`, `_read_attribute`, `attributes_for_database`, the dirty-tracking helpers (`*_was`,
247
+ `changes`, `changes_to_save`), `Marshal.dump` and `to_yaml` / `YAML.dump`. Use `FiltersResponseAttributes` on
248
+ hashes you build this way.
249
+ - **Presence:** `where(email: value).exists?` tells whether a value is stored, and `attribute_present?(:email)`
250
+ answers for the stored value even when the column is hidden (the `email?` reader follows the guard).
251
+ - **Rows:** with `auto_query_scoping = :off`, everything except class-level `find_by` / `find_by!` and
252
+ `permission_scoped` (for example `find`, `where`, `scope.find_by`, `association.find_by`). With auto-scoping on,
253
+ `unscoped`, `find_by_sql` and raw SQL still skip it; PostgreSQL RLS covers them for connections that set the user's
254
+ session variable (with `nil_user_strategy = :bypass`, a connection without it sees every row).
255
+ - **Writes that skip callbacks** (`update_all`, `delete_all`, raw SQL) are not audited.
256
+
257
+ Details: [Masked Attributes](https://github.com/fetijashari/permission_forge/blob/main/docs/masked-attributes.md),
258
+ [Query Scoping](https://github.com/fetijashari/permission_forge/blob/main/docs/query-scoping.md).
259
+
260
+ ---
261
+
262
+ ## Audit log
263
+
264
+ Every create, update and destroy, made through the models, of a Permission Set, Resource, field, action, rule, group,
265
+ assignment or membership is written to the `audit_events` table inside the change's own transaction. Under the default
266
+ `audit_change_failure_strategy = :raise`, a change whose event cannot be written rolls back. Each event stores the
267
+ actor, source, request id, IP, an optional reason, a correlation id, and the changes or a snapshot of the deleted row.
268
+ Name the person who acts, otherwise HTTP Basic admins share one name:
269
+
270
+ ```ruby
271
+ PermissionForge.configure do |config|
272
+ config.audit_actor_resolver = ->(controller) { controller.current_admin }
273
+ config.audit_retention_period = 2.years # purge: bin/rails permission_forge:audit_events:purge
274
+ end
275
+ ```
276
+
277
+ Read it at `<mount path>/audit`, export it as CSV or JSON, or call `GET /api/audit_events`. It supports evidence for
278
+ access-control and accountability requirements; it is not a certification. It does not record writes that skip
279
+ callbacks or, by default, who read data. The model refuses updates and deletes, but `update_all`, `delete_all` and
280
+ direct database access still reach the table, so protect it at database level, forward it, synchronise clocks and set
281
+ retention. See [Audit Log](https://github.com/fetijashari/permission_forge/blob/main/docs/audit-log.md).
282
+
283
+ ## Documentation
284
+
285
+ | Guide | Description |
286
+ |-------|-------------|
287
+ | [Getting Started](https://github.com/fetijashari/permission_forge/blob/main/docs/getting-started.md) | Install, first Permission Set, first checks |
288
+ | [Configuration](https://github.com/fetijashari/permission_forge/blob/main/docs/configuration.md) | Every option and its default |
289
+ | [Architecture](https://github.com/fetijashari/permission_forge/blob/main/docs/architecture.md) | Data model, merge rules, cache and invalidation |
290
+ | [Usage](https://github.com/fetijashari/permission_forge/blob/main/docs/usage.md) | Checking access, custom actions, fields, masking, rules, filtering |
291
+ | [Masked Attributes](https://github.com/fetijashari/permission_forge/blob/main/docs/masked-attributes.md) | Model-level field enforcement and what it does not cover |
292
+ | [Field Filtering](https://github.com/fetijashari/permission_forge/blob/main/docs/field-filtering.md) | Read-path and write-path filtering, nested attributes, virtual fields |
293
+ | [Row-Level Rules](https://github.com/fetijashari/permission_forge/blob/main/docs/row-level-rules.md) | Operators, join types, association paths, STI |
294
+ | [Query Scoping](https://github.com/fetijashari/permission_forge/blob/main/docs/query-scoping.md) | Auto-scoping, unscoped-query detection, PostgreSQL RLS |
295
+ | [Muting Permission Sets](https://github.com/fetijashari/permission_forge/blob/main/docs/muting-permission-sets.md) | Subtracting access |
296
+ | [Activation Conditions](https://github.com/fetijashari/permission_forge/blob/main/docs/activation-conditions.md) | Session flags, IP allowlists, login hours |
297
+ | [Assignment Expiration](https://github.com/fetijashari/permission_forge/blob/main/docs/assignment-expiration.md) | Time-bound assignments and clean-up |
298
+ | [Permission Set Groups](https://github.com/fetijashari/permission_forge/blob/main/docs/permission-set-groups.md) | Grouping Permission Sets |
299
+ | [Controller Actions](https://github.com/fetijashari/permission_forge/blob/main/docs/controller-actions.md) | Permit or block controller actions per Permission Set |
300
+ | [Admin UI](https://github.com/fetijashari/permission_forge/blob/main/docs/admin-ui.md) | Mounting, authentication, screens |
301
+ | [Admin JSON API](https://github.com/fetijashari/permission_forge/blob/main/docs/admin-api.md) | Authentication, CSRF, endpoints, pagination, serializers |
302
+ | [Audit Log](https://github.com/fetijashari/permission_forge/blob/main/docs/audit-log.md) | What is recorded and what is not, exports, retention, protecting the table |
303
+ | [Multi-Tenancy](https://github.com/fetijashari/permission_forge/blob/main/docs/multi-tenancy.md) | `acts_as_tenant` integration |
304
+ | [Security Audit](https://github.com/fetijashari/permission_forge/blob/main/docs/security-audit.md) | `rake permission_forge:audit` and its findings |
305
+ | [GDPR Data Handling](https://github.com/fetijashari/permission_forge/blob/main/docs/gdpr-data-handling.md) | What the gem evidences and what the host must add |
306
+ | [HIPAA](https://github.com/fetijashari/permission_forge/blob/main/docs/hipaa-compliance.md) | Technical safeguards: what the gem evidences and what the host must add |
307
+ | [Masking Disclaimer](https://github.com/fetijashari/permission_forge/blob/main/docs/gdpr_masking_disclaimer.md) | Masking is not anonymization |
308
+ | [Table Name Prefix](https://github.com/fetijashari/permission_forge/blob/main/docs/table-prefix.md) | Custom prefixes, moving between prefixes |
309
+ | [Internationalization](https://github.com/fetijashari/permission_forge/blob/main/docs/i18n.md) | Locales and overrides |
310
+ | [Performance](https://github.com/fetijashari/permission_forge/blob/main/docs/benchmarks.md) | Overhead and cache costs |
311
+ | [Testing](https://github.com/fetijashari/permission_forge/blob/main/docs/testing.md) | Testing permissions in your app |
312
+ | [Migration Guide](https://github.com/fetijashari/permission_forge/blob/main/docs/migration-guide.md) | Upgrading between versions |
313
+ | [Tips & Helpers](https://github.com/fetijashari/permission_forge/blob/main/docs/tips-and-helpers.md) | Common patterns and pitfalls |
314
+ | [AI assistant instructions](https://github.com/fetijashari/permission_forge/blob/main/docs/claude-instructions.md) | A section to paste into your app's `CLAUDE.md` / `AGENTS.md` |
315
+
316
+ ---
317
+
318
+ ## Versioning and stability
319
+
320
+ PermissionForge follows [Semantic Versioning](https://semver.org/) and is pre-1.0. Until 1.0, a minor version (0.x.0)
321
+ may change defaults and behaviour; every such change is listed under **Changed** in the
322
+ [CHANGELOG](https://github.com/fetijashari/permission_forge/blob/main/CHANGELOG.md) with its opt-out and upgrade
323
+ steps. Patch versions (0.x.y) only fix bugs. The supported surface is the configuration options, the concerns your
324
+ models include, the services documented in the guides, the error classes and the permission hash format
325
+ ([Architecture](https://github.com/fetijashari/permission_forge/blob/main/docs/architecture.md)). Other classes are
326
+ internal and may change in any release: classes marked `@api private` in the source (and hidden in the YARD docs) are
327
+ internal. A public API is never renamed or removed in one step: the release that changes it keeps the old name
328
+ working for at least one minor version and warns through `PermissionForge.deprecator` (registered with
329
+ `Rails.application.deprecators`, so `config.active_support.deprecation` governs it), and the CHANGELOG lists it under
330
+ **Deprecated** with the version that removes it. Security fixes go into the latest release only.
331
+
332
+ ## Security
333
+
334
+ Please report vulnerabilities privately, as described in
335
+ [SECURITY.md](https://github.com/fetijashari/permission_forge/blob/main/SECURITY.md).
336
+
337
+ ## Contributing
338
+
339
+ Bug reports and pull requests are welcome. See
340
+ [CONTRIBUTING.md](https://github.com/fetijashari/permission_forge/blob/main/CONTRIBUTING.md) for the setup, the test
341
+ suites and the rules the code follows, and the
342
+ [Code of Conduct](https://github.com/fetijashari/permission_forge/blob/main/CODE_OF_CONDUCT.md).
343
+
344
+ ## Acknowledgements
345
+
346
+ PermissionForge's permission model (resource-level CRUD, field-level visibility and row-level query rules) was
347
+ inspired by [Salesforce's Profile and Permission Set architecture](https://help.salesforce.com/s/articleView?id=sf.perm_sets_overview.htm&type=5),
348
+ adapted for Ruby on Rails.
349
+
350
+ Built at [Confidee](https://confidee.com), where it controls access across internal operations, customer portals and
351
+ supplier portals.
352
+
353
+ ## License
354
+
355
+ The gem is available as open source under the terms of the
356
+ [MIT License](https://github.com/fetijashari/permission_forge/blob/main/LICENSE.txt).
@@ -0,0 +1,17 @@
1
+ module PermissionForge
2
+ # The acting admin as a record whose id can be stored (reviewed_by_id,
3
+ # sensitive_acknowledged_by_id, created_by_id), or nil — under HTTP Basic
4
+ # or a resolver that returns something that is not a record. The audit
5
+ # event's actor comes from the audit context either way.
6
+ #
7
+ # @api private
8
+ module ActingAdmin
9
+ private
10
+
11
+ # @return [Object, nil] pf_current_admin when it has an id
12
+ def pf_acting_admin_record
13
+ admin = pf_current_admin
14
+ admin.respond_to?(:id) ? admin : nil
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Api
5
+ # Template Method concern providing standard CRUD actions for API controllers.
6
+ #
7
+ # Include this concern and override the private template methods to customize
8
+ # behavior per controller. Override any action method for non-standard behavior.
9
+ #
10
+ # @example
11
+ # class ResourceFieldsController < BaseController
12
+ # include CrudActions
13
+ #
14
+ # private
15
+ #
16
+ # def collection_scope = @resource.resource_fields
17
+ # def current_record = @field
18
+ # def serializer_type = :resource_field
19
+ # def record_params = pf_require_hash(:resource_field).permit(:name, :label)
20
+ # end
21
+ module CrudActions
22
+ extend ActiveSupport::Concern
23
+
24
+ def index
25
+ render_collection(collection_scope.order(sort_column), serializer_for(serializer_type))
26
+ end
27
+
28
+ def show
29
+ render_record(current_record, serializer_for(serializer_type))
30
+ end
31
+
32
+ def create
33
+ record = build_record
34
+ guard_mutation!(:create, record)
35
+ return if performed?
36
+
37
+ if record.save
38
+ render_created(record, serializer_for(serializer_type))
39
+ else
40
+ render_validation_errors(record)
41
+ end
42
+ end
43
+
44
+ def update
45
+ guard_mutation!(:update, current_record)
46
+ return if performed?
47
+
48
+ if current_record.update(record_params)
49
+ render_record(current_record, serializer_for(serializer_type))
50
+ else
51
+ render_validation_errors(current_record)
52
+ end
53
+ end
54
+
55
+ def destroy
56
+ guard_mutation!(:destroy, current_record)
57
+ return if performed?
58
+
59
+ current_record.destroy!
60
+ head :no_content
61
+ end
62
+
63
+ private
64
+
65
+ # @return [ActiveRecord::Relation] the parent collection scope
66
+ def collection_scope = raise(NotImplementedError, "#{self.class}#collection_scope")
67
+
68
+ # @return [ActiveRecord::Base] the record for show/update/destroy
69
+ def current_record = raise(NotImplementedError, "#{self.class}#current_record")
70
+
71
+ # @return [Symbol] key for serializer_for()
72
+ def serializer_type = raise(NotImplementedError, "#{self.class}#serializer_type")
73
+
74
+ # @return [ActionController::Parameters] permitted params
75
+ def record_params = raise(NotImplementedError, "#{self.class}#record_params")
76
+
77
+ # @return [ActiveRecord::Base] new record built from collection_scope and params
78
+ def build_record = collection_scope.build(record_params)
79
+
80
+ # @return [Symbol] default sort column for index
81
+ def sort_column = :name
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,18 @@
1
+ module PermissionForge
2
+ module Api
3
+ module NestedResourceLookup
4
+ extend ActiveSupport::Concern
5
+
6
+ private
7
+
8
+ def set_resource
9
+ @permission_set = find_record(PermissionForge::PermissionSet, :permission_set_identifier)
10
+ @resource = find_record(@permission_set.resources, :resource_identifier)
11
+ end
12
+
13
+ def find_nested_record(scope)
14
+ find_record(scope)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,50 @@
1
+ module PermissionForge
2
+ module Api
3
+ module Pagination
4
+ extend ActiveSupport::Concern
5
+
6
+ # Beyond this the offset overflows the database's integer; the page is
7
+ # empty long before.
8
+ MAX_PAGE = (2**31) - 1
9
+
10
+ private
11
+
12
+ def paginate_scope(scope, paginator)
13
+ page = current_page
14
+ per = clamped_per_page
15
+ paginator.call(scope, page, per)
16
+ end
17
+
18
+ def default_paginate_scope(scope)
19
+ page = current_page
20
+ per = clamped_per_page
21
+ total = scope.count
22
+ records = scope.limit(per).offset((page - 1) * per)
23
+ records.define_singleton_method(:total_count) { total }
24
+ records
25
+ end
26
+
27
+ def pagination_meta(paginated)
28
+ {
29
+ pagination: {
30
+ page: current_page,
31
+ per_page: clamped_per_page,
32
+ total_count: paginated.respond_to?(:total_count) ? paginated.total_count : paginated.size
33
+ }
34
+ }
35
+ end
36
+
37
+ # Anything that is not a number (a Hash, an Array) reads as page 1.
38
+ def current_page
39
+ params[:page].to_s.to_i.clamp(1, MAX_PAGE)
40
+ end
41
+
42
+ def clamped_per_page
43
+ config = PermissionForge.configuration
44
+ requested = params[:per_page]
45
+ requested = requested.nil? ? config.api_default_per_page : requested.to_s.to_i
46
+ requested.clamp(1, config.api_max_per_page)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,65 @@
1
+ module PermissionForge
2
+ module Api
3
+ module ResponseRenderer
4
+ extend ActiveSupport::Concern
5
+
6
+ private
7
+
8
+ def render_record(record, serializer_class)
9
+ if ams_serializer?(serializer_class)
10
+ render json: record, serializer: serializer_class, root: :data, status: :ok
11
+ else
12
+ render json: { data: serializer_class.new(record).as_json }, status: :ok
13
+ end
14
+ end
15
+
16
+ def render_created(record, serializer_class)
17
+ if ams_serializer?(serializer_class)
18
+ render json: record, serializer: serializer_class, root: :data, status: :created
19
+ else
20
+ render json: { data: serializer_class.new(record).as_json }, status: :created
21
+ end
22
+ end
23
+
24
+ def render_collection(scope, serializer_class)
25
+ paginator = PermissionForge.configuration.api_pagination_handler
26
+ records = paginator ? paginate_scope(scope, paginator) : default_paginate_scope(scope)
27
+ meta = pagination_meta(records)
28
+
29
+ if ams_serializer?(serializer_class)
30
+ render json: records, each_serializer: serializer_class, root: :data, meta: meta, status: :ok
31
+ else
32
+ data = serializer_class.collection(records)
33
+ render json: { data: data, meta: meta }, status: :ok
34
+ end
35
+ end
36
+
37
+ def render_validation_errors(record)
38
+ render json: { error: { status: 422, messages: record.errors.messages } }, status: :unprocessable_entity
39
+ end
40
+
41
+ # Saves a record and renders the created resource or its validation errors.
42
+ def save_and_render(record, serializer_type)
43
+ if record.save
44
+ render_created(record, serializer_for(serializer_type))
45
+ else
46
+ render_validation_errors(record)
47
+ end
48
+ end
49
+
50
+ def render_not_authorized(message = nil)
51
+ msg = message || I18n.t('permission_forge.api.errors.forbidden')
52
+ render json: { error: { status: 403, messages: msg } }, status: :forbidden
53
+ end
54
+
55
+ def render_not_found(message = nil)
56
+ msg = message || I18n.t('permission_forge.api.errors.not_found')
57
+ render json: { error: { status: 404, messages: msg } }, status: :not_found
58
+ end
59
+
60
+ def ams_serializer?(serializer_class)
61
+ defined?(ActiveModel::Serializer) && serializer_class < ActiveModel::Serializer
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,30 @@
1
+ module PermissionForge
2
+ module Api
3
+ module SerializerResolver
4
+ extend ActiveSupport::Concern
5
+
6
+ private
7
+
8
+ def serializer_for(resource_type)
9
+ overrides = PermissionForge.configuration.api_serializer_overrides
10
+ return overrides[resource_type] if overrides.key?(resource_type)
11
+
12
+ default_serializers.fetch(resource_type)
13
+ end
14
+
15
+ def default_serializers
16
+ {
17
+ permission_set: PermissionForge::Api::Serializers::PermissionSetSerializer,
18
+ resource: PermissionForge::Api::Serializers::ResourceSerializer,
19
+ resource_field: PermissionForge::Api::Serializers::ResourceFieldSerializer,
20
+ resource_action: PermissionForge::Api::Serializers::ResourceActionSerializer,
21
+ permission_set_rule: PermissionForge::Api::Serializers::PermissionSetRuleSerializer,
22
+ permission_set_user: PermissionForge::Api::Serializers::PermissionSetUserSerializer,
23
+ permission_set_group: PermissionForge::Api::Serializers::PermissionSetGroupSerializer,
24
+ permission_set_group_user: PermissionForge::Api::Serializers::PermissionSetGroupUserSerializer,
25
+ audit_event: PermissionForge::Api::Serializers::AuditEventSerializer
26
+ }
27
+ end
28
+ end
29
+ end
30
+ end