permission_forge 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (406) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +1138 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +356 -0
  5. data/app/controllers/concerns/permission_forge/acting_admin.rb +17 -0
  6. data/app/controllers/concerns/permission_forge/api/crud_actions.rb +84 -0
  7. data/app/controllers/concerns/permission_forge/api/nested_resource_lookup.rb +18 -0
  8. data/app/controllers/concerns/permission_forge/api/pagination.rb +50 -0
  9. data/app/controllers/concerns/permission_forge/api/response_renderer.rb +65 -0
  10. data/app/controllers/concerns/permission_forge/api/serializer_resolver.rb +30 -0
  11. data/app/controllers/concerns/permission_forge/api_authentication.rb +40 -0
  12. data/app/controllers/concerns/permission_forge/assignment_authorization.rb +97 -0
  13. data/app/controllers/concerns/permission_forge/audit_log_gate.rb +55 -0
  14. data/app/controllers/concerns/permission_forge/basic_authentication.rb +77 -0
  15. data/app/controllers/concerns/permission_forge/cache_confirmation.rb +32 -0
  16. data/app/controllers/concerns/permission_forge/forgery_protection.rb +29 -0
  17. data/app/controllers/concerns/permission_forge/html_format.rb +40 -0
  18. data/app/controllers/concerns/permission_forge/param_shapes.rb +87 -0
  19. data/app/controllers/concerns/permission_forge/tenant_required.rb +42 -0
  20. data/app/controllers/permission_forge/api/audit_events_controller.rb +33 -0
  21. data/app/controllers/permission_forge/api/base_controller.rb +220 -0
  22. data/app/controllers/permission_forge/api/dashboard_controller.rb +30 -0
  23. data/app/controllers/permission_forge/api/permission_set_group_users_controller.rb +51 -0
  24. data/app/controllers/permission_forge/api/permission_set_groups_controller.rb +71 -0
  25. data/app/controllers/permission_forge/api/permission_set_rules_controller.rb +37 -0
  26. data/app/controllers/permission_forge/api/permission_set_users_controller.rb +105 -0
  27. data/app/controllers/permission_forge/api/permission_sets_controller.rb +87 -0
  28. data/app/controllers/permission_forge/api/resource_actions_controller.rb +58 -0
  29. data/app/controllers/permission_forge/api/resource_fields_controller.rb +30 -0
  30. data/app/controllers/permission_forge/api/resources_controller.rb +56 -0
  31. data/app/controllers/permission_forge/application_controller.rb +198 -0
  32. data/app/controllers/permission_forge/assets_controller.rb +97 -0
  33. data/app/controllers/permission_forge/assignments_controller.rb +266 -0
  34. data/app/controllers/permission_forge/audit_events_controller.rb +94 -0
  35. data/app/controllers/permission_forge/bulk_assignments_controller.rb +108 -0
  36. data/app/controllers/permission_forge/field_lookups_controller.rb +38 -0
  37. data/app/controllers/permission_forge/home_controller.rb +24 -0
  38. data/app/controllers/permission_forge/maintenance_controller.rb +13 -0
  39. data/app/controllers/permission_forge/people_controller.rb +190 -0
  40. data/app/controllers/permission_forge/permission_set_editor.rb +71 -0
  41. data/app/controllers/permission_forge/permission_set_group_users_controller.rb +57 -0
  42. data/app/controllers/permission_forge/permission_set_groups_controller.rb +111 -0
  43. data/app/controllers/permission_forge/permission_set_rules_controller.rb +97 -0
  44. data/app/controllers/permission_forge/permission_sets_controller.rb +227 -0
  45. data/app/controllers/permission_forge/preferences_controller.rb +26 -0
  46. data/app/controllers/permission_forge/resource_actions_controller.rb +106 -0
  47. data/app/controllers/permission_forge/resource_fields_controller.rb +129 -0
  48. data/app/controllers/permission_forge/resources_controller.rb +189 -0
  49. data/app/controllers/permission_forge/settings_controller.rb +33 -0
  50. data/app/frontend/permission_forge/brand/apple-touch-icon.png +0 -0
  51. data/app/frontend/permission_forge/brand/chevron.svg +1 -0
  52. data/app/frontend/permission_forge/brand/favicon.ico +0 -0
  53. data/app/frontend/permission_forge/brand/favicon.svg +1 -0
  54. data/app/frontend/permission_forge/brand/logo-horizontal-dark.svg +1 -0
  55. data/app/frontend/permission_forge/brand/logo-horizontal-light.svg +1 -0
  56. data/app/frontend/permission_forge/brand/mark-small.svg +1 -0
  57. data/app/frontend/permission_forge/brand/mark.svg +1 -0
  58. data/app/frontend/permission_forge/pf_admin.css +766 -0
  59. data/app/frontend/permission_forge/pf_admin.js +482 -0
  60. data/app/helpers/permission_forge/application_helper.rb +167 -0
  61. data/app/helpers/permission_forge/audit_helper.rb +230 -0
  62. data/app/helpers/permission_forge/audit_sentences_helper.rb +84 -0
  63. data/app/helpers/permission_forge/badges_helper.rb +134 -0
  64. data/app/helpers/permission_forge/changes_helper.rb +117 -0
  65. data/app/helpers/permission_forge/people_helper.rb +118 -0
  66. data/app/helpers/permission_forge/sentences_helper.rb +175 -0
  67. data/app/helpers/permission_forge/settings_helper.rb +152 -0
  68. data/app/helpers/permission_forge/ui_helper.rb +184 -0
  69. data/app/models/permission_forge/audit_event.rb +159 -0
  70. data/app/models/permission_forge/permission_set.rb +100 -0
  71. data/app/models/permission_forge/permission_set_group.rb +83 -0
  72. data/app/models/permission_forge/permission_set_group_user.rb +43 -0
  73. data/app/models/permission_forge/permission_set_rule.rb +99 -0
  74. data/app/models/permission_forge/permission_set_user.rb +158 -0
  75. data/app/models/permission_forge/resource.rb +100 -0
  76. data/app/models/permission_forge/resource_action.rb +58 -0
  77. data/app/models/permission_forge/resource_field.rb +69 -0
  78. data/app/views/layouts/permission_forge/application.html.erb +30 -0
  79. data/app/views/layouts/permission_forge/fragment.html.erb +2 -0
  80. data/app/views/permission_forge/assignments/_person_options.html.erb +26 -0
  81. data/app/views/permission_forge/assignments/confirm_destroy.html.erb +24 -0
  82. data/app/views/permission_forge/assignments/new.html.erb +99 -0
  83. data/app/views/permission_forge/audit_events/index.html.erb +161 -0
  84. data/app/views/permission_forge/audit_events/show.html.erb +146 -0
  85. data/app/views/permission_forge/bulk_assignments/confirm_revoke.html.erb +21 -0
  86. data/app/views/permission_forge/bulk_assignments/new.html.erb +69 -0
  87. data/app/views/permission_forge/field_lookups/show.html.erb +32 -0
  88. data/app/views/permission_forge/home/_audit_card.html.erb +19 -0
  89. data/app/views/permission_forge/home/_ending_card.html.erb +32 -0
  90. data/app/views/permission_forge/home/_expired_card.html.erb +34 -0
  91. data/app/views/permission_forge/home/_privacy_card.html.erb +67 -0
  92. data/app/views/permission_forge/home/_rules_card.html.erb +23 -0
  93. data/app/views/permission_forge/home/_stale_card.html.erb +39 -0
  94. data/app/views/permission_forge/home/_tidy_card.html.erb +18 -0
  95. data/app/views/permission_forge/home/index.html.erb +69 -0
  96. data/app/views/permission_forge/people/_access_tab.html.erb +58 -0
  97. data/app/views/permission_forge/people/_audit_tab.html.erb +93 -0
  98. data/app/views/permission_forge/people/_effective_field_source.html.erb +11 -0
  99. data/app/views/permission_forge/people/_effective_tab.html.erb +118 -0
  100. data/app/views/permission_forge/people/_effective_why.html.erb +48 -0
  101. data/app/views/permission_forge/people/_why_item.html.erb +19 -0
  102. data/app/views/permission_forge/people/confirm_remove_all.html.erb +27 -0
  103. data/app/views/permission_forge/people/consent_preview.html.erb +39 -0
  104. data/app/views/permission_forge/people/erasure_preview.html.erb +28 -0
  105. data/app/views/permission_forge/people/index.html.erb +78 -0
  106. data/app/views/permission_forge/people/show.html.erb +54 -0
  107. data/app/views/permission_forge/permission_set_groups/confirm_destroy.html.erb +12 -0
  108. data/app/views/permission_forge/permission_set_groups/index.html.erb +55 -0
  109. data/app/views/permission_forge/permission_set_groups/show.html.erb +100 -0
  110. data/app/views/permission_forge/permission_set_rules/_builder.html.erb +145 -0
  111. data/app/views/permission_forge/permission_set_rules/_value_input.html.erb +42 -0
  112. data/app/views/permission_forge/permission_set_rules/confirm_destroy.html.erb +13 -0
  113. data/app/views/permission_forge/permission_set_rules/edit.html.erb +3 -0
  114. data/app/views/permission_forge/permission_set_rules/new.html.erb +3 -0
  115. data/app/views/permission_forge/permission_sets/_assignments_tab.html.erb +74 -0
  116. data/app/views/permission_forge/permission_sets/_audit_tab.html.erb +80 -0
  117. data/app/views/permission_forge/permission_sets/_conditions_tab.html.erb +83 -0
  118. data/app/views/permission_forge/permission_sets/_header.html.erb +36 -0
  119. data/app/views/permission_forge/permission_sets/_records_summary.html.erb +16 -0
  120. data/app/views/permission_forge/permission_sets/_resource_summary.html.erb +11 -0
  121. data/app/views/permission_forge/permission_sets/_resources_tab.html.erb +49 -0
  122. data/app/views/permission_forge/permission_sets/confirm_destroy.html.erb +23 -0
  123. data/app/views/permission_forge/permission_sets/confirm_switch_mode.html.erb +15 -0
  124. data/app/views/permission_forge/permission_sets/index.html.erb +83 -0
  125. data/app/views/permission_forge/permission_sets/new.html.erb +63 -0
  126. data/app/views/permission_forge/permission_sets/review.html.erb +26 -0
  127. data/app/views/permission_forge/permission_sets/show.html.erb +50 -0
  128. data/app/views/permission_forge/resource_actions/_across_sets.html.erb +24 -0
  129. data/app/views/permission_forge/resource_actions/_add_select.html.erb +12 -0
  130. data/app/views/permission_forge/resource_actions/_effect.html.erb +25 -0
  131. data/app/views/permission_forge/resource_actions/_endpoints.html.erb +16 -0
  132. data/app/views/permission_forge/resource_actions/_form.html.erb +44 -0
  133. data/app/views/permission_forge/resource_actions/_managed_note.html.erb +11 -0
  134. data/app/views/permission_forge/resource_actions/confirm_destroy.html.erb +13 -0
  135. data/app/views/permission_forge/resource_actions/new.html.erb +31 -0
  136. data/app/views/permission_forge/resource_actions/show.html.erb +39 -0
  137. data/app/views/permission_forge/resource_fields/_explain.html.erb +24 -0
  138. data/app/views/permission_forge/resource_fields/show.html.erb +186 -0
  139. data/app/views/permission_forge/resources/_actions_tab.html.erb +88 -0
  140. data/app/views/permission_forge/resources/_fields_tab.html.erb +89 -0
  141. data/app/views/permission_forge/resources/_rules_tab.html.erb +55 -0
  142. data/app/views/permission_forge/resources/_sync.html.erb +57 -0
  143. data/app/views/permission_forge/resources/confirm_destroy.html.erb +17 -0
  144. data/app/views/permission_forge/resources/new.html.erb +57 -0
  145. data/app/views/permission_forge/resources/show.html.erb +117 -0
  146. data/app/views/permission_forge/settings/_ladder.html.erb +15 -0
  147. data/app/views/permission_forge/settings/_row.html.erb +38 -0
  148. data/app/views/permission_forge/settings/show.html.erb +67 -0
  149. data/app/views/permission_forge/shared/_across_sets.html.erb +31 -0
  150. data/app/views/permission_forge/shared/_audit_events.html.erb +36 -0
  151. data/app/views/permission_forge/shared/_audit_footer.html.erb +16 -0
  152. data/app/views/permission_forge/shared/_audit_off_note.html.erb +15 -0
  153. data/app/views/permission_forge/shared/_audit_pagination.html.erb +19 -0
  154. data/app/views/permission_forge/shared/_audit_refused_note.html.erb +7 -0
  155. data/app/views/permission_forge/shared/_audit_timeline.html.erb +29 -0
  156. data/app/views/permission_forge/shared/_crud_cell.html.erb +18 -0
  157. data/app/views/permission_forge/shared/_dialog_frame.html.erb +11 -0
  158. data/app/views/permission_forge/shared/_expiry_picker.html.erb +27 -0
  159. data/app/views/permission_forge/shared/_head_extra.html.erb +3 -0
  160. data/app/views/permission_forge/shared/_help_banner.html.erb +10 -0
  161. data/app/views/permission_forge/shared/_level_control.html.erb +18 -0
  162. data/app/views/permission_forge/shared/_mode_banners.html.erb +22 -0
  163. data/app/views/permission_forge/shared/_nav.html.erb +12 -0
  164. data/app/views/permission_forge/shared/_preferences.html.erb +24 -0
  165. data/app/views/permission_forge/shared/_reason_field.html.erb +20 -0
  166. data/app/views/permission_forge/shared/_savebar.html.erb +13 -0
  167. data/app/views/permission_forge/shared/_sheet_frame.html.erb +22 -0
  168. data/app/views/permission_forge/shared/_sidebar.html.erb +18 -0
  169. data/app/views/permission_forge/shared/_tech_details.html.erb +20 -0
  170. data/app/views/permission_forge/shared/_toasts.html.erb +27 -0
  171. data/app/views/permission_forge/shared/_topbar.html.erb +15 -0
  172. data/app/views/permission_forge/shared/_user_select.html.erb +16 -0
  173. data/app/views/permission_forge/shared/audit_refused.html.erb +7 -0
  174. data/config/locales/permission_forge.en.yml +2115 -0
  175. data/config/locales/permission_forge.mk.yml +2166 -0
  176. data/config/locales/permission_forge.sq.yml +2164 -0
  177. data/config/routes.rb +143 -0
  178. data/lib/generators/permission_forge/install/install_generator.rb +105 -0
  179. data/lib/generators/permission_forge/install/templates/initializer.rb.tt +148 -0
  180. data/lib/generators/permission_forge/install/templates/migration.rb.tt +198 -0
  181. data/lib/generators/permission_forge/rls/rls_generator.rb +57 -0
  182. data/lib/generators/permission_forge/rls/templates/enable_permission_forge_rls.rb.tt +15 -0
  183. data/lib/generators/permission_forge/upgrade/templates/add_admin_ui_columns.rb.tt +8 -0
  184. data/lib/generators/permission_forge/upgrade/templates/add_audit_events.rb.tt +98 -0
  185. data/lib/generators/permission_forge/upgrade/templates/add_resource_fields_unique_index.rb.tt +69 -0
  186. data/lib/generators/permission_forge/upgrade/upgrade_generator.rb +66 -0
  187. data/lib/permission_forge/admin/action_across_sets.rb +76 -0
  188. data/lib/permission_forge/admin/action_rows.rb +156 -0
  189. data/lib/permission_forge/admin/attention_items.rb +207 -0
  190. data/lib/permission_forge/admin/audit_event_detail.rb +75 -0
  191. data/lib/permission_forge/admin/audit_log.rb +193 -0
  192. data/lib/permission_forge/admin/audit_log_days.rb +41 -0
  193. data/lib/permission_forge/admin/audit_lookups.rb +105 -0
  194. data/lib/permission_forge/admin/audit_name_history.rb +85 -0
  195. data/lib/permission_forge/admin/audit_trail.rb +169 -0
  196. data/lib/permission_forge/admin/bulk_people_matcher.rb +109 -0
  197. data/lib/permission_forge/admin/change_set.rb +123 -0
  198. data/lib/permission_forge/admin/change_set_parts/actions.rb +27 -0
  199. data/lib/permission_forge/admin/change_set_parts/base.rb +58 -0
  200. data/lib/permission_forge/admin/change_set_parts/context.rb +50 -0
  201. data/lib/permission_forge/admin/change_set_parts/field_edit.rb +67 -0
  202. data/lib/permission_forge/admin/change_set_parts/field_levels.rb +71 -0
  203. data/lib/permission_forge/admin/change_set_parts/new_actions.rb +91 -0
  204. data/lib/permission_forge/admin/change_set_parts/new_fields.rb +63 -0
  205. data/lib/permission_forge/admin/change_set_parts/resource_edit.rb +55 -0
  206. data/lib/permission_forge/admin/change_set_parts/resource_flags.rb +19 -0
  207. data/lib/permission_forge/admin/change_set_parts/set_attrs.rb +45 -0
  208. data/lib/permission_forge/admin/change_set_parts/values.rb +20 -0
  209. data/lib/permission_forge/admin/column_fields.rb +48 -0
  210. data/lib/permission_forge/admin/conditions_form.rb +78 -0
  211. data/lib/permission_forge/admin/controller_catalog.rb +166 -0
  212. data/lib/permission_forge/admin/csv.rb +41 -0
  213. data/lib/permission_forge/admin/editable_lock.rb +157 -0
  214. data/lib/permission_forge/admin/effective_access.rb +158 -0
  215. data/lib/permission_forge/admin/effective_fields.rb +176 -0
  216. data/lib/permission_forge/admin/expiry_presets.rb +51 -0
  217. data/lib/permission_forge/admin/exports/document.rb +89 -0
  218. data/lib/permission_forge/admin/exports/events.rb +51 -0
  219. data/lib/permission_forge/admin/exports/holdings.rb +118 -0
  220. data/lib/permission_forge/admin/exports.rb +37 -0
  221. data/lib/permission_forge/admin/field_across_sets.rb +51 -0
  222. data/lib/permission_forge/admin/field_detail.rb +112 -0
  223. data/lib/permission_forge/admin/field_level.rb +74 -0
  224. data/lib/permission_forge/admin/field_rows.rb +137 -0
  225. data/lib/permission_forge/admin/last_changes.rb +94 -0
  226. data/lib/permission_forge/admin/model_catalog.rb +216 -0
  227. data/lib/permission_forge/admin/people_candidates.rb +108 -0
  228. data/lib/permission_forge/admin/people_directory.rb +158 -0
  229. data/lib/permission_forge/admin/permission_set_factory.rb +48 -0
  230. data/lib/permission_forge/admin/person_label.rb +95 -0
  231. data/lib/permission_forge/admin/record_id.rb +19 -0
  232. data/lib/permission_forge/admin/rule_builder.rb +58 -0
  233. data/lib/permission_forge/admin/rule_form.rb +175 -0
  234. data/lib/permission_forge/admin/rule_form_input.rb +74 -0
  235. data/lib/permission_forge/admin/sensitive_columns.rb +68 -0
  236. data/lib/permission_forge/admin/settings_catalog.rb +261 -0
  237. data/lib/permission_forge/admin/settings_values.rb +130 -0
  238. data/lib/permission_forge/api/serializers/audit_event_serializer.rb +27 -0
  239. data/lib/permission_forge/api/serializers/base_serializer.rb +36 -0
  240. data/lib/permission_forge/api/serializers/permission_set_group_serializer.rb +12 -0
  241. data/lib/permission_forge/api/serializers/permission_set_group_user_serializer.rb +11 -0
  242. data/lib/permission_forge/api/serializers/permission_set_rule_serializer.rb +25 -0
  243. data/lib/permission_forge/api/serializers/permission_set_serializer.rb +13 -0
  244. data/lib/permission_forge/api/serializers/permission_set_user_serializer.rb +11 -0
  245. data/lib/permission_forge/api/serializers/resource_action_serializer.rb +13 -0
  246. data/lib/permission_forge/api/serializers/resource_field_serializer.rb +14 -0
  247. data/lib/permission_forge/api/serializers/resource_serializer.rb +15 -0
  248. data/lib/permission_forge/audit/actor.rb +144 -0
  249. data/lib/permission_forge/audit/actor_resolver.rb +103 -0
  250. data/lib/permission_forge/audit/audited_views.rb +208 -0
  251. data/lib/permission_forge/audit/boot_warnings.rb +58 -0
  252. data/lib/permission_forge/audit/column_values.rb +56 -0
  253. data/lib/permission_forge/audit/context.rb +98 -0
  254. data/lib/permission_forge/audit/detached.rb +62 -0
  255. data/lib/permission_forge/audit/event_catalog.rb +110 -0
  256. data/lib/permission_forge/audit/event_query.rb +192 -0
  257. data/lib/permission_forge/audit/failure.rb +47 -0
  258. data/lib/permission_forge/audit/field_access.rb +186 -0
  259. data/lib/permission_forge/audit/gate.rb +167 -0
  260. data/lib/permission_forge/audit/job_context.rb +32 -0
  261. data/lib/permission_forge/audit/log_access.rb +36 -0
  262. data/lib/permission_forge/audit/recorder.rb +168 -0
  263. data/lib/permission_forge/audit/refusals.rb +61 -0
  264. data/lib/permission_forge/audit/request_context.rb +152 -0
  265. data/lib/permission_forge/audit/runtime.rb +178 -0
  266. data/lib/permission_forge/audit/runtime_emission.rb +135 -0
  267. data/lib/permission_forge/audit/settings_observer.rb +70 -0
  268. data/lib/permission_forge/audit/source.rb +54 -0
  269. data/lib/permission_forge/audit/subscribers/base.rb +69 -0
  270. data/lib/permission_forge/audit/subscribers/legacy_hooks.rb +97 -0
  271. data/lib/permission_forge/audit/subscribers/log.rb +45 -0
  272. data/lib/permission_forge/audit/subscribers.rb +58 -0
  273. data/lib/permission_forge/audit.rb +302 -0
  274. data/lib/permission_forge/boot_checks.rb +184 -0
  275. data/lib/permission_forge/cache/backlog.rb +142 -0
  276. data/lib/permission_forge/cache/base.rb +200 -0
  277. data/lib/permission_forge/cache/bulk_invalidation.rb +131 -0
  278. data/lib/permission_forge/cache/failure.rb +171 -0
  279. data/lib/permission_forge/cache/handled_errors.rb +82 -0
  280. data/lib/permission_forge/cache/holder_invalidation.rb +85 -0
  281. data/lib/permission_forge/cache/invalidation.rb +180 -0
  282. data/lib/permission_forge/cache/memory_cache.rb +77 -0
  283. data/lib/permission_forge/cache/null_cache.rb +40 -0
  284. data/lib/permission_forge/cache/rails_cache.rb +231 -0
  285. data/lib/permission_forge/cache/rebuild.rb +149 -0
  286. data/lib/permission_forge/cache/settlement.rb +63 -0
  287. data/lib/permission_forge/cache/user_keys.rb +206 -0
  288. data/lib/permission_forge/cache/variant_removal.rb +55 -0
  289. data/lib/permission_forge/cache/withheld_notice.rb +60 -0
  290. data/lib/permission_forge/concerns/audited_collection.rb +34 -0
  291. data/lib/permission_forge/concerns/auto_permission_scoping.rb +158 -0
  292. data/lib/permission_forge/concerns/controller_action_guard.rb +106 -0
  293. data/lib/permission_forge/concerns/field_mask_checker.rb +45 -0
  294. data/lib/permission_forge/concerns/has_active_scope.rb +12 -0
  295. data/lib/permission_forge/concerns/has_audit_trail.rb +59 -0
  296. data/lib/permission_forge/concerns/has_permission_set_users.rb +115 -0
  297. data/lib/permission_forge/concerns/has_tenant_scoping.rb +86 -0
  298. data/lib/permission_forge/concerns/has_uuid.rb +24 -0
  299. data/lib/permission_forge/concerns/invalidates_in_batch.rb +207 -0
  300. data/lib/permission_forge/concerns/invalidates_managed_actions.rb +80 -0
  301. data/lib/permission_forge/concerns/masked_attributes/alias_access.rb +280 -0
  302. data/lib/permission_forge/concerns/masked_attributes/store_access.rb +100 -0
  303. data/lib/permission_forge/concerns/masked_attributes/validation_reads.rb +153 -0
  304. data/lib/permission_forge/concerns/masked_attributes/virtual_readers.rb +242 -0
  305. data/lib/permission_forge/concerns/masked_attributes.rb +576 -0
  306. data/lib/permission_forge/concerns/permission_cache_accessor.rb +416 -0
  307. data/lib/permission_forge/concerns/permission_scoped_findable.rb +337 -0
  308. data/lib/permission_forge/concerns/permissioned_user.rb +78 -0
  309. data/lib/permission_forge/concerns/permissions_memo.rb +160 -0
  310. data/lib/permission_forge/concerns/permissions_per_context.rb +288 -0
  311. data/lib/permission_forge/concerns/permissions_regenerator.rb +43 -0
  312. data/lib/permission_forge/concerns/records_audit_events.rb +286 -0
  313. data/lib/permission_forge/concerns/resource_permission_checker.rb +116 -0
  314. data/lib/permission_forge/concerns/validates_column_lengths.rb +69 -0
  315. data/lib/permission_forge/concerns/validates_dependency.rb +54 -0
  316. data/lib/permission_forge/conditional_sets.rb +361 -0
  317. data/lib/permission_forge/configuration/admin_ui_settings.rb +108 -0
  318. data/lib/permission_forge/configuration/audit_settings.rb +184 -0
  319. data/lib/permission_forge/configuration/cache_settings.rb +76 -0
  320. data/lib/permission_forge/configuration/controller_action_settings.rb +112 -0
  321. data/lib/permission_forge/configuration/gdpr_settings.rb +70 -0
  322. data/lib/permission_forge/configuration/masked_attributes_settings.rb +83 -0
  323. data/lib/permission_forge/configuration/permits_attributes_settings.rb +26 -0
  324. data/lib/permission_forge/configuration/presets.rb +139 -0
  325. data/lib/permission_forge/configuration/query_scoping_settings.rb +107 -0
  326. data/lib/permission_forge/configuration/strategy_values.rb +57 -0
  327. data/lib/permission_forge/configuration/virtual_attributes_settings.rb +65 -0
  328. data/lib/permission_forge/configuration.rb +624 -0
  329. data/lib/permission_forge/controller_actions/authorizer.rb +171 -0
  330. data/lib/permission_forge/controller_actions/managed_index.rb +213 -0
  331. data/lib/permission_forge/controller_actions/mapping.rb +167 -0
  332. data/lib/permission_forge/controller_actions.rb +122 -0
  333. data/lib/permission_forge/deprecation.rb +86 -0
  334. data/lib/permission_forge/engine.rb +113 -0
  335. data/lib/permission_forge/execution_state.rb +318 -0
  336. data/lib/permission_forge/invalidation_batch.rb +206 -0
  337. data/lib/permission_forge/masked_models.rb +226 -0
  338. data/lib/permission_forge/masking_sync.rb +271 -0
  339. data/lib/permission_forge/model_bindings.rb +90 -0
  340. data/lib/permission_forge/open_transaction.rb +303 -0
  341. data/lib/permission_forge/operators/constants.rb +41 -0
  342. data/lib/permission_forge/operators/registry.rb +300 -0
  343. data/lib/permission_forge/operators/rule_column.rb +120 -0
  344. data/lib/permission_forge/operators/validations.rb +111 -0
  345. data/lib/permission_forge/operators/value_casters/number.rb +68 -0
  346. data/lib/permission_forge/operators/value_casters/temporal.rb +69 -0
  347. data/lib/permission_forge/operators/value_casters.rb +80 -0
  348. data/lib/permission_forge/operators/value_typing.rb +115 -0
  349. data/lib/permission_forge/query_scoping_warnings.rb +68 -0
  350. data/lib/permission_forge/rake_audit.rb +65 -0
  351. data/lib/permission_forge/resource_chain.rb +190 -0
  352. data/lib/permission_forge/rls.rb +209 -0
  353. data/lib/permission_forge/services/attribute_access_guard.rb +138 -0
  354. data/lib/permission_forge/services/audit_event_purger.rb +150 -0
  355. data/lib/permission_forge/services/bulk_permission_set_assignment.rb +108 -0
  356. data/lib/permission_forge/services/concerns/across_tenants.rb +69 -0
  357. data/lib/permission_forge/services/concerns/audited_operation.rb +43 -0
  358. data/lib/permission_forge/services/concerns/clonable.rb +29 -0
  359. data/lib/permission_forge/services/consent_withdrawal_handler.rb +135 -0
  360. data/lib/permission_forge/services/dashboard_stats.rb +43 -0
  361. data/lib/permission_forge/services/data_subject_erasure.rb +306 -0
  362. data/lib/permission_forge/services/expired_assignment_cleaner.rb +152 -0
  363. data/lib/permission_forge/services/extra_field_defaults.rb +71 -0
  364. data/lib/permission_forge/services/field_permission_merger.rb +83 -0
  365. data/lib/permission_forge/services/filters_response_attributes.rb +144 -0
  366. data/lib/permission_forge/services/gdpr_field_auditor/text_report.rb +63 -0
  367. data/lib/permission_forge/services/gdpr_field_auditor.rb +223 -0
  368. data/lib/permission_forge/services/host_key_guard.rb +110 -0
  369. data/lib/permission_forge/services/mask_value.rb +93 -0
  370. data/lib/permission_forge/services/permission_cache_clearer.rb +174 -0
  371. data/lib/permission_forge/services/permission_hash_builder.rb +305 -0
  372. data/lib/permission_forge/services/permission_rule_applier.rb +192 -0
  373. data/lib/permission_forge/services/permission_set_cloner.rb +89 -0
  374. data/lib/permission_forge/services/permission_set_condition_evaluator.rb +102 -0
  375. data/lib/permission_forge/services/permission_set_group_cloner.rb +52 -0
  376. data/lib/permission_forge/services/permits_attributes/nested_records.rb +106 -0
  377. data/lib/permission_forge/services/permits_attributes.rb +270 -0
  378. data/lib/permission_forge/services/resource_field_generator.rb +93 -0
  379. data/lib/permission_forge/services/resource_with_fields_builder.rb +47 -0
  380. data/lib/permission_forge/services/rls_operator_sql.rb +131 -0
  381. data/lib/permission_forge/services/rls_policy_generator.rb +365 -0
  382. data/lib/permission_forge/services/security_audit/action_check.rb +157 -0
  383. data/lib/permission_forge/services/security_audit/assignment_check.rb +120 -0
  384. data/lib/permission_forge/services/security_audit/audit_trail_check.rb +166 -0
  385. data/lib/permission_forge/services/security_audit/base_check.rb +57 -0
  386. data/lib/permission_forge/services/security_audit/config_check.rb +212 -0
  387. data/lib/permission_forge/services/security_audit/integrity_check.rb +105 -0
  388. data/lib/permission_forge/services/security_audit/personal_data_check.rb +79 -0
  389. data/lib/permission_forge/services/security_audit/privilege_check.rb +67 -0
  390. data/lib/permission_forge/services/security_audit/rule_check.rb +141 -0
  391. data/lib/permission_forge/services/security_audit/text_report.rb +46 -0
  392. data/lib/permission_forge/services/security_audit/user_model_check.rb +67 -0
  393. data/lib/permission_forge/services/security_audit.rb +75 -0
  394. data/lib/permission_forge/services/stale_assignment_auditor.rb +68 -0
  395. data/lib/permission_forge/services/unscoped_query_detector.rb +132 -0
  396. data/lib/permission_forge/version.rb +3 -0
  397. data/lib/permission_forge/virtual_fields/discovery.rb +65 -0
  398. data/lib/permission_forge/virtual_fields.rb +102 -0
  399. data/lib/permission_forge.rb +464 -0
  400. data/lib/tasks/permission_forge.rake +31 -0
  401. data/lib/tasks/permission_forge_audit_events.rake +28 -0
  402. data/lib/tasks/permission_forge_cache.rake +26 -0
  403. data/lib/tasks/permission_forge_expiration.rake +30 -0
  404. data/lib/tasks/permission_forge_gdpr.rake +69 -0
  405. data/lib/tasks/permission_forge_rls.rake +38 -0
  406. metadata +515 -0
@@ -0,0 +1,100 @@
1
+ module PermissionForge
2
+ # A named group of resource, field, action and rule grants (or, with `is_muting`, of revocations)
3
+ # that is assigned to users.
4
+ #
5
+ # @api public
6
+ class PermissionSet < ActiveRecord::Base
7
+ # The table name, with the configured prefix (read on every call, so a prefix set at runtime applies).
8
+ def self.table_name = "#{PermissionForge.table_name_prefix}permission_sets"
9
+
10
+ include PermissionForge::Concerns::HasActiveScope
11
+ include PermissionForge::Concerns::HasUuid
12
+ include PermissionForge::Concerns::ValidatesColumnLengths
13
+ include PermissionForge::Concerns::HasAuditTrail
14
+ include PermissionForge::Concerns::HasTenantScoping
15
+ include PermissionForge::Concerns::HasPermissionSetUsers
16
+ include PermissionForge::Concerns::PermissionsRegenerator
17
+ include PermissionForge::Concerns::InvalidatesManagedActions
18
+ include PermissionForge::Concerns::RecordsAuditEvents
19
+
20
+ permission_set_user_lookup :self
21
+
22
+ belongs_to :permission_set_group, class_name: 'PermissionForge::PermissionSetGroup', optional: true
23
+ # Every collection removes through callbacks (an audit event each):
24
+ # `dependent: :destroy` on :users destroys the assignment rows, never a
25
+ # person; AuditedCollection covers `clear` / `delete_all`.
26
+ has_many :resources, class_name: 'PermissionForge::Resource', dependent: :destroy, extend: Concerns::AuditedCollection
27
+ has_many :permission_set_rules, class_name: 'PermissionForge::PermissionSetRule', dependent: :destroy,
28
+ extend: Concerns::AuditedCollection
29
+ has_many :permission_set_users, class_name: 'PermissionForge::PermissionSetUser', dependent: :destroy,
30
+ inverse_of: :permission_set, extend: Concerns::AuditedCollection
31
+ has_many :users, through: :permission_set_users, dependent: :destroy, extend: Concerns::AuditedCollection
32
+
33
+ # Checked for a new row and on a rename only: a row whose name was
34
+ # blank before 0.14 stays editable in its other attributes.
35
+ validates :name, presence: { message: ->(*) { I18n.t('permission_forge.validations.name_blank') } },
36
+ if: -> { new_record? || will_save_change_to_name? }
37
+ validates :name, length: { maximum: 255 }
38
+ # A group id must name a group (of this tenant): optional, but never dangling.
39
+ validates :permission_set_group,
40
+ presence: { message: ->(*) { I18n.t('permission_forge.validations.permission_set.group_missing') } },
41
+ if: -> { permission_set_group_id.present? && will_save_change_to_permission_set_group_id? }
42
+ validates :allowed_hours_start, numericality: { in: 0..23, allow_nil: true }
43
+ validates :allowed_hours_end, numericality: { in: 0..23, allow_nil: true }
44
+
45
+ scope :muting, -> { where(is_muting: true) }
46
+ scope :granting, -> { where(is_muting: false) }
47
+ scope :conditional, lambda {
48
+ where.not(requires_session_flag: [nil, ''])
49
+ .or(where.not(allowed_ips: [nil, '']))
50
+ .or(where.not(allowed_hours_start: nil))
51
+ }
52
+ scope :unconditional, lambda {
53
+ where(requires_session_flag: [nil, ''])
54
+ .where(allowed_ips: [nil, ''])
55
+ .where(allowed_hours_start: nil)
56
+ }
57
+
58
+ apply_tenant_scoping! # from the configuration, as the class loads (HasTenantScoping)
59
+
60
+ def active?
61
+ is_active
62
+ end
63
+
64
+ def inactive?
65
+ !is_active
66
+ end
67
+
68
+ def muting?
69
+ is_muting
70
+ end
71
+
72
+ def granting?
73
+ !is_muting
74
+ end
75
+
76
+ def conditional?
77
+ requires_session_flag.present? || allowed_ips.present? || allowed_hours_start.present?
78
+ end
79
+
80
+ def unconditional?
81
+ !conditional?
82
+ end
83
+
84
+ # Deep-clones this permission set with all resources, fields, and rules.
85
+ # Does NOT copy user assignments — the clone starts with no users.
86
+ def deep_clone(attrs = {})
87
+ PermissionForge::Services::PermissionSetCloner.call(permission_set: self, attrs: attrs)
88
+ end
89
+
90
+ private
91
+
92
+ # InvalidatesManagedActions: a new set has no resources yet and the index
93
+ # holds no set names, so only switching a set on or off changes what is
94
+ # managed. (Its resources drop the index themselves when they are added
95
+ # or destroyed.)
96
+ def managed_actions_changed?
97
+ !previously_new_record? && saved_change_to_is_active?
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,83 @@
1
+ module PermissionForge
2
+ # An optional grouping of Permission Sets that is assigned to people as one.
3
+ #
4
+ # @api public
5
+ class PermissionSetGroup < ActiveRecord::Base
6
+ # The table name, with the configured prefix (read on every call, so a prefix set at runtime applies).
7
+ def self.table_name = "#{PermissionForge.table_name_prefix}permission_set_groups"
8
+
9
+ include PermissionForge::Concerns::HasActiveScope
10
+ include PermissionForge::Concerns::HasUuid
11
+ include PermissionForge::Concerns::ValidatesColumnLengths
12
+ include PermissionForge::Concerns::HasAuditTrail
13
+ include PermissionForge::Concerns::HasTenantScoping
14
+ include PermissionForge::Concerns::PermissionsRegenerator
15
+ include PermissionForge::Concerns::RecordsAuditEvents
16
+
17
+ # Deleting a group detaches its sets (detach_permission_sets) — not
18
+ # `dependent: :nullify`, whose UPDATE runs no callbacks and so leaves no
19
+ # audit event. A set removed from the collection (`delete`, `ids=`, `=`,
20
+ # `clear`) is detached the same way by `before_remove`, so Rails' own
21
+ # nullify that follows finds nothing left to change. Never
22
+ # `dependent: :destroy` here: that would delete the sets.
23
+ has_many :permission_sets, class_name: 'PermissionForge::PermissionSet',
24
+ before_remove: :detach_permission_set, extend: Concerns::AuditedCollection
25
+ has_many :permission_set_group_users, class_name: 'PermissionForge::PermissionSetGroupUser', dependent: :destroy,
26
+ extend: Concerns::AuditedCollection
27
+ # Destroys the membership rows, never a person.
28
+ has_many :users, through: :permission_set_group_users, dependent: :destroy, extend: Concerns::AuditedCollection
29
+
30
+ # Checked for a new row and on a rename only: a row whose name was
31
+ # blank before 0.14 stays editable in its other attributes.
32
+ validates :name, presence: { message: ->(*) { I18n.t('permission_forge.validations.name_blank') } },
33
+ if: -> { new_record? || will_save_change_to_name? }
34
+
35
+ before_destroy :detach_permission_sets
36
+
37
+ apply_tenant_scoping! # from the configuration, as the class loads (HasTenantScoping)
38
+
39
+ # Deep-clones this group with all its permission sets and their resources/fields/rules.
40
+ # Does NOT copy user assignments — the clone starts with no users.
41
+ def deep_clone(attrs = {})
42
+ PermissionForge::Services::PermissionSetGroupCloner.call(permission_set_group: self, attrs: attrs)
43
+ end
44
+
45
+ private
46
+
47
+ # before_remove of #permission_sets: an audited save (a
48
+ # `permission_set.updated` event) instead of Rails' UPDATE. The row is
49
+ # what counts, not the instance: one loaded before the set joined this
50
+ # group still has the old foreign key, so the stored row is detached.
51
+ def detach_permission_set(set)
52
+ return unless set.persisted?
53
+
54
+ set = in_record_tenant(PermissionForge::PermissionSet).find_by(id: set.id, permission_set_group_id: id) unless
55
+ set.permission_set_group_id == id
56
+ return unless set
57
+
58
+ set.permission_set_group_id = nil
59
+ set.save!(validate: false)
60
+ end
61
+
62
+ # One audited save per set (a `permission_set.updated` event each, in the
63
+ # destroy's transaction, so they share its correlation id). The sets are
64
+ # read in the group's own tenant. Validation is skipped: a set that an
65
+ # unrelated rule would now refuse (a legacy over-long name) must still let
66
+ # go of a group that is being deleted.
67
+ def detach_permission_sets
68
+ in_record_tenant(PermissionForge::PermissionSet).where(permission_set_group_id: id).find_each do |set|
69
+ set.permission_set_group_id = nil
70
+ set.save!(validate: false)
71
+ end
72
+ end
73
+
74
+ def users_on_permission_set
75
+ return [] unless is_active
76
+
77
+ ps_ids = in_record_tenant(PermissionForge::PermissionSet).where(permission_set_group_id: id, is_active: true).pluck(:id)
78
+ return [] if ps_ids.empty?
79
+
80
+ in_record_tenant(PermissionForge::PermissionSetUser).where(permission_set_id: ps_ids)
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,43 @@
1
+ module PermissionForge
2
+ # The membership of a user in a Permission Set Group (unique per user and group).
3
+ #
4
+ # @api public
5
+ class PermissionSetGroupUser < ActiveRecord::Base
6
+ # The table name, with the configured prefix (read on every call, so a prefix set at runtime applies).
7
+ def self.table_name = "#{PermissionForge.table_name_prefix}permission_set_group_users"
8
+
9
+ include PermissionForge::Concerns::HasUuid
10
+ include PermissionForge::Concerns::ValidatesColumnLengths
11
+ include PermissionForge::Concerns::HasAuditTrail
12
+ include PermissionForge::Concerns::HasTenantScoping
13
+ include PermissionForge::Concerns::RecordsAuditEvents
14
+
15
+ # optional: is stated (see PermissionSetUser): the group is optional as
16
+ # declared since 0.13, the user is required.
17
+ belongs_to :permission_set_group, class_name: 'PermissionForge::PermissionSetGroup', optional: true
18
+ belongs_to :user, class_name: PermissionForge.configuration.user_class, optional: false
19
+
20
+ # Mirrors the DB unique index so a repeat assignment returns a 422 instead of
21
+ # a RecordNotUnique 500 (and prevents silent duplicate rows on schemas that
22
+ # predate the index).
23
+ validates :user_id, uniqueness: { scope: :permission_set_group_id }
24
+
25
+ # See PermissionSetUser: never store a truncated user key.
26
+ before_validation { PermissionForge::Services::HostKeyGuard.verify! }
27
+
28
+ # The reason of the change in progress (Audit::Context) is kept on the row.
29
+ before_save :keep_audit_reason
30
+
31
+ apply_tenant_scoping! # from the configuration, as the class loads (HasTenantScoping)
32
+
33
+ private
34
+
35
+ def keep_audit_reason
36
+ reason = PermissionForge::Audit::Context[:reason]
37
+ return if reason.blank? || !has_attribute?(:reason)
38
+ return unless new_record? || changed?
39
+
40
+ self.reason = PermissionForge::Audit::ColumnValues.value(reason.to_s)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,99 @@
1
+ module PermissionForge
2
+ # A row-level query rule of a Permission Set: one operator applied to one field of one resource.
3
+ #
4
+ # @api public
5
+ class PermissionSetRule < ActiveRecord::Base
6
+ # The table name, with the configured prefix (read on every call, so a prefix set at runtime applies).
7
+ def self.table_name = "#{PermissionForge.table_name_prefix}permission_set_rules"
8
+
9
+ include PermissionForge::Concerns::HasActiveScope
10
+ include PermissionForge::Operators::Constants
11
+ include PermissionForge::Concerns::HasUuid
12
+ include PermissionForge::Concerns::ValidatesColumnLengths
13
+ include PermissionForge::Concerns::HasAuditTrail
14
+ include PermissionForge::Concerns::HasTenantScoping
15
+ include PermissionForge::Concerns::HasPermissionSetUsers
16
+ include PermissionForge::Concerns::PermissionsRegenerator
17
+ include PermissionForge::Concerns::RecordsAuditEvents
18
+
19
+ permission_set_user_lookup :direct
20
+
21
+ # optional: as declared since 0.13 (the foreign keys hold); stated, so the
22
+ # host's belongs_to_required_by_default decides nothing here.
23
+ belongs_to :permission_set, class_name: 'PermissionForge::PermissionSet', optional: true
24
+ belongs_to :resource, class_name: 'PermissionForge::Resource', optional: true
25
+ scope :recently_deactivated, -> { where(is_active: false).where('updated_at > ?', 7.days.ago) }
26
+
27
+ # join_type values that mean "filter on the model's own table". Any other
28
+ # join_type is a join rule: auto_query_scoping skips it, so it only applies
29
+ # through find_by/find_by! and permission_scoped.
30
+ FILTER_JOIN_TYPES = [nil, '', 'filter'].freeze
31
+ scope :join_type_rules, -> { where.not(join_type: FILTER_JOIN_TYPES) }
32
+
33
+ # Whether a join_type means "filter on the model's own table": nothing,
34
+ # blank or 'filter'. The one definition — the applier, the default scope
35
+ # and the RLS generator all ask here, so a row written past the model's
36
+ # validation is read the same way by all three.
37
+ def self.filter_join_type?(join_type)
38
+ join_type.blank? || join_type.to_s == 'filter'
39
+ end
40
+
41
+ def join_rule?
42
+ !self.class.filter_join_type?(join_type)
43
+ end
44
+
45
+ validates :operator, inclusion: {
46
+ in: OPERATORS,
47
+ message: I18n.t('permission_forge.validations.permission_set_rule.invalid_operator',
48
+ default: 'is not a valid operator')
49
+ }
50
+ # An operator that takes no value (Operators::Validations.valueless?)
51
+ # stores an empty one: the column is NOT NULL, and the natural API payload
52
+ # for a null check has no field_value.
53
+ before_validation { self.field_value = '' if field_value.nil? && valueless_operator? }
54
+
55
+ validates :field_value, presence: {
56
+ message: I18n.t('permission_forge.validations.permission_set_rule.field_value_required',
57
+ default: 'is required for this operator')
58
+ }, unless: :valueless_operator?
59
+ validates :join_type, inclusion: {
60
+ in: JOIN_TYPES,
61
+ message: I18n.t('permission_forge.validations.permission_set_rule.invalid_join_type',
62
+ default: 'is not a valid join type')
63
+ }, allow_nil: true, allow_blank: false
64
+ validates :field_name, presence: true
65
+ validates :field_name, format: {
66
+ with: /\A[a-zA-Z_][a-zA-Z0-9_.]*\z/,
67
+ message: I18n.t('permission_forge.validations.permission_set_rule.invalid_field_name',
68
+ default: 'must be a valid column name (letters, numbers, underscores, dots)')
69
+ }, allow_blank: true
70
+
71
+ validate :validate_field_name_segments, if: :field_name_changed?
72
+ validate :validate_field_value_for_operator
73
+
74
+ apply_tenant_scoping! # from the configuration, as the class loads (HasTenantScoping)
75
+
76
+ private
77
+
78
+ def valueless_operator?
79
+ PermissionForge::Operators::Validations.valueless?(operator)
80
+ end
81
+
82
+ # `flag.` or `a..b` passes the format above, but an empty segment makes it
83
+ # another name than the one written (see Operators::RuleColumn.name?).
84
+ # Checked for a new rule and on a rename only: a row that already carries
85
+ # such a name stays editable in its other attributes (it matches nothing).
86
+ def validate_field_name_segments
87
+ return if field_name.blank? || errors[:field_name].any?
88
+ return if PermissionForge::Operators::RuleColumn.name?(field_name)
89
+
90
+ errors.add(:field_name, I18n.t('permission_forge.validations.permission_set_rule.invalid_field_name',
91
+ default: 'must be a valid column name (letters, numbers, underscores, dots)'))
92
+ end
93
+
94
+ def validate_field_value_for_operator
95
+ result = PermissionForge::Operators::Validations.validate(operator, field_value)
96
+ errors.add(result[:field], result[:message]) if result
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,158 @@
1
+ module PermissionForge
2
+ # The assignment of a Permission Set to a user (unique per user and set), optionally with an `expires_at`.
3
+ #
4
+ # @api public
5
+ class PermissionSetUser < ActiveRecord::Base
6
+ # The table name, with the configured prefix (read on every call, so a prefix set at runtime applies).
7
+ def self.table_name = "#{PermissionForge.table_name_prefix}permission_set_users"
8
+
9
+ include PermissionForge::Concerns::HasUuid
10
+ include PermissionForge::Concerns::ValidatesColumnLengths
11
+ include PermissionForge::Concerns::HasAuditTrail
12
+ include PermissionForge::Concerns::HasTenantScoping
13
+ include PermissionForge::Concerns::InvalidatesInBatch
14
+ include PermissionForge::Concerns::RecordsAuditEvents
15
+
16
+ # optional: is stated on every belongs_to: the host's
17
+ # belongs_to_required_by_default decides nothing here. The parent is
18
+ # optional, as declared since 0.13 (the foreign key holds); the user is
19
+ # required.
20
+ belongs_to :permission_set, class_name: 'PermissionForge::PermissionSet', optional: true
21
+ belongs_to :user, class_name: PermissionForge.configuration.user_class, optional: false
22
+
23
+ # Mirrors the DB unique index so a repeat assignment returns a 422 instead of
24
+ # a RecordNotUnique 500 (and prevents silent duplicate rows on schemas that
25
+ # predate the index). BulkPermissionSetAssignment already de-dupes; this
26
+ # covers the single-assignment controller/API paths.
27
+ validates :user_id, uniqueness: { scope: :permission_set_id }
28
+
29
+ # An integer user_id column would store a UUID or string user key as its
30
+ # leading digits, handing this assignment to every user who shares them.
31
+ before_validation { PermissionForge::Services::HostKeyGuard.verify! }
32
+
33
+ # The reason of the change in progress (Audit::Context, e.g. a ticket
34
+ # from the request) is kept on the row it changed.
35
+ before_save :keep_audit_reason
36
+
37
+ # A move to another user takes the set from the previous holder: noted
38
+ # at every save, invalidated with the holder after the commit
39
+ # (InvalidatesInBatch#note_previous_value).
40
+ after_save { note_previous_value('user_id') }
41
+
42
+ scope :active_assignments, -> { where(expires_at: nil).or(where(expires_at: Time.current..)) }
43
+ scope :expired, -> { where.not(expires_at: nil).where(expires_at: ...Time.current) }
44
+
45
+ # Permanent assignments last reviewed before `cutoff` — the access
46
+ # review list and StaleAssignmentAuditor. Reviewed means a "Keep"
47
+ # (reviewed_at), else the grant itself (created_at); a schema without
48
+ # the review columns (upgrade migration not run) keeps measuring from
49
+ # updated_at, as before.
50
+ scope :stale_before, ->(cutoff) { where(expires_at: nil).where(last_reviewed_column.lt(cutoff)) }
51
+
52
+ apply_tenant_scoping! # from the configuration, as the class loads (HasTenantScoping)
53
+
54
+ # The review columns exist (bin/rails generate permission_forge:upgrade).
55
+ def self.review_supported?
56
+ column_names.include?('reviewed_at')
57
+ end
58
+
59
+ # Arel for "when this assignment was last reviewed" (see stale_before).
60
+ def self.last_reviewed_column
61
+ table = arel_table
62
+ return table[:updated_at] unless review_supported?
63
+
64
+ Arel::Nodes::NamedFunction.new('COALESCE', [table[:reviewed_at], table[:created_at]])
65
+ end
66
+
67
+ # When this assignment was last reviewed (see stale_before).
68
+ def last_reviewed_at
69
+ return updated_at unless self.class.review_supported?
70
+
71
+ reviewed_at || created_at
72
+ end
73
+
74
+ # Access review "Keep": records who reviewed the assignment and when,
75
+ # through a normal update that writes one assignment.reviewed event
76
+ # (with the decision; the reason comes from Audit::Context). Without the
77
+ # review columns only updated_at moves, so the event is recorded itself.
78
+ #
79
+ # @param reviewer [Object, nil] the acting admin (its id is stored)
80
+ # @param decision [String]
81
+ # @return [self]
82
+ def mark_reviewed!(reviewer:, decision: 'keep')
83
+ data = { 'decision' => decision }
84
+ if self.class.review_supported?
85
+ attrs = { reviewed_at: Time.current }
86
+ attrs[:reviewed_by_id] = reviewer&.id if has_attribute?(:reviewed_by_id)
87
+ audit_as('assignment.reviewed', data: data) { update!(attrs) }
88
+ else
89
+ record_review_without_columns(data)
90
+ end
91
+ self
92
+ end
93
+
94
+ def deactivated?
95
+ active_check = PermissionForge.configuration.user_active_check
96
+ !active_check.call(user)
97
+ end
98
+
99
+ def expired?
100
+ expires_at.present? && expires_at < Time.current
101
+ end
102
+
103
+ def active_assignment?
104
+ !expired?
105
+ end
106
+
107
+ private
108
+
109
+ def record_review_without_columns(data)
110
+ transaction do
111
+ update!(updated_at: Time.current)
112
+ PermissionForge::Audit.record('assignment.reviewed', record: self, data: data)
113
+ end
114
+ end
115
+
116
+ def keep_audit_reason
117
+ reason = PermissionForge::Audit::Context[:reason]
118
+ return if reason.blank? || !has_attribute?(:reason)
119
+ return unless new_record? || changed?
120
+
121
+ self.reason = PermissionForge::Audit::ColumnValues.value(reason.to_s)
122
+ end
123
+
124
+ # InvalidatesInBatch (after commit, on create/update/destroy).
125
+ #
126
+ # Assigning or revoking a set must take effect immediately: invalidate the
127
+ # user's cached hash so the next read rebuilds it (read-through). This also
128
+ # covers PermissionSet#destroy — dependent: :destroy removes each assignment
129
+ # through callbacks, so every previously-assigned user is invalidated even
130
+ # though the set's own after_commit can no longer find the deleted rows.
131
+ def invalidate_committed_caches
132
+ super
133
+ invalidate_user_permissions
134
+ end
135
+
136
+ def invalidate_user_permissions
137
+ holders = [user_id, *previous_values('user_id')]
138
+ invalidate_assigned_users(holders, invalidation_batch, permission_record_tenant_id)
139
+ end
140
+
141
+ # RecordsAuditEvents: the event of a move names the previous holder too
142
+ # (its subject is the new one; data["changes"]["user_id"] has both).
143
+ def audit_update_details
144
+ return {} unless saved_change_to_user_id?
145
+
146
+ { 'previous_subject_user_id' => user_id_before_last_save&.to_s }
147
+ end
148
+
149
+ # The holder of this assignment is at hand when the assignment was built
150
+ # with it (`create!(user: user)`): no query to load it again.
151
+ def assigned_users(user_ids)
152
+ holder = association(:user).target
153
+ return [holder] if holder && user_ids == [holder.id]
154
+
155
+ super
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,100 @@
1
+ module PermissionForge
2
+ # Entity-level CRUD access to one model (its `name`) inside one Permission Set.
3
+ #
4
+ # @api public
5
+ class Resource < ActiveRecord::Base
6
+ # The table name, with the configured prefix (read on every call, so a prefix set at runtime applies).
7
+ def self.table_name = "#{PermissionForge.table_name_prefix}resources"
8
+
9
+ include PermissionForge::Concerns::HasActiveScope
10
+ include PermissionForge::Concerns::HasUuid
11
+ include PermissionForge::Concerns::ValidatesColumnLengths
12
+ include PermissionForge::Concerns::HasAuditTrail
13
+ include PermissionForge::Concerns::HasTenantScoping
14
+ include PermissionForge::Concerns::HasPermissionSetUsers
15
+ include PermissionForge::Concerns::ValidatesDependency
16
+ include PermissionForge::Concerns::PermissionsRegenerator
17
+ include PermissionForge::Concerns::InvalidatesManagedActions
18
+ include PermissionForge::Concerns::RecordsAuditEvents
19
+
20
+ # A Ruby constant path: Resource#name is resolved to a model class.
21
+ NAME_FORMAT = PermissionForge::RESOURCE_NAME_FORMAT
22
+
23
+ permission_set_user_lookup :direct
24
+
25
+ # Active rows of active Permission Sets — the rows that count right now.
26
+ scope :in_effect, -> { active.joins(:permission_set).merge(PermissionForge::PermissionSet.active) }
27
+
28
+ # A model named for the first time is masked in this process and every
29
+ # other one (MaskingSync). Noted per save, consumed once committed.
30
+ after_save :note_newly_named_model
31
+ after_commit :announce_newly_named_model
32
+
33
+ # optional: as declared since 0.13 (the foreign key holds); stated, so the
34
+ # host's belongs_to_required_by_default decides nothing here.
35
+ belongs_to :permission_set, class_name: 'PermissionForge::PermissionSet', optional: true
36
+ has_many :resource_fields, class_name: 'PermissionForge::ResourceField', dependent: :destroy,
37
+ extend: Concerns::AuditedCollection
38
+ has_many :resource_actions, class_name: 'PermissionForge::ResourceAction', dependent: :destroy,
39
+ extend: Concerns::AuditedCollection
40
+ has_many :permission_set_rules, class_name: 'PermissionForge::PermissionSetRule', dependent: :destroy,
41
+ extend: Concerns::AuditedCollection
42
+
43
+ validates :name, presence: true
44
+ validates :name, uniqueness: { scope: :permission_set_id, message: 'already exists in this permission set' }
45
+ # Resource#name is resolved to a model class via safe_constantize (e.g. by
46
+ # ResourceFieldGenerator and MaskedAttributes auto-apply), so constrain it to
47
+ # a Ruby constant path. Prevents naming a resource after arbitrary strings.
48
+ validates :name, format: {
49
+ with: NAME_FORMAT,
50
+ message: I18n.t('permission_forge.validations.resource.invalid_name',
51
+ default: 'must be a valid model class name (e.g. Customer or Admin::Report)')
52
+ }, allow_blank: true
53
+
54
+ validates_dependency :edit_access, requires: :read_access
55
+ validates_dependency :delete_access, requires: :read_access
56
+ validates_dependency :modify_all, requires: :view_all
57
+
58
+ apply_tenant_scoping! # from the configuration, as the class loads (HasTenantScoping)
59
+
60
+ # Fallback for installations that haven't added the label column yet.
61
+ def label
62
+ has_attribute?(:label) ? read_attribute(:label) : name
63
+ end
64
+
65
+ # For a loaded record: active, in an active Permission Set (see .in_effect).
66
+ def in_effect?
67
+ is_active && permission_set.is_active
68
+ end
69
+
70
+ # View All / Modify All skip every record rule on this resource.
71
+ def rules_bypassed?
72
+ view_all || modify_all
73
+ end
74
+
75
+ # Grants full CRUD access + all field access for a resource on a permission set.
76
+ # Intended for seeds, migrations, or console use only.
77
+ def self.grant_full_access!(permission_set:, resource_name:)
78
+ PermissionForge::Services::ResourceWithFieldsBuilder.call(
79
+ permission_set: permission_set, resource_name: resource_name
80
+ )
81
+ end
82
+
83
+ private
84
+
85
+ def note_newly_named_model
86
+ @pf_newly_named = true if previously_new_record? || saved_change_to_name?
87
+ end
88
+
89
+ # The concern is applied here whatever the other rows; the other
90
+ # processes are told when no other row (in any tenant) had the name, or
91
+ # this process had not masked the model yet (MaskingSync.named!, which
92
+ # asks for the other rows only when the answer decides something).
93
+ def announce_newly_named_model
94
+ return unless @pf_newly_named && persisted?
95
+
96
+ @pf_newly_named = false
97
+ PermissionForge::MaskingSync.named!(name) { self.class.unscoped.where(name: name).where.not(id: id).exists? }
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,58 @@
1
+ module PermissionForge
2
+ # A custom action (such as sync or export) that a Permission Set permits or withholds on a resource.
3
+ #
4
+ # @api public
5
+ class ResourceAction < ActiveRecord::Base
6
+ # The table name, with the configured prefix (read on every call, so a prefix set at runtime applies).
7
+ def self.table_name = "#{PermissionForge.table_name_prefix}resource_actions"
8
+
9
+ include PermissionForge::Concerns::HasActiveScope
10
+ include PermissionForge::Concerns::HasUuid
11
+ include PermissionForge::Concerns::ValidatesColumnLengths
12
+ include PermissionForge::Concerns::HasAuditTrail
13
+ include PermissionForge::Concerns::HasTenantScoping
14
+ include PermissionForge::Concerns::HasPermissionSetUsers
15
+ include PermissionForge::Concerns::PermissionsRegenerator
16
+ include PermissionForge::Concerns::InvalidatesManagedActions
17
+ include PermissionForge::Concerns::RecordsAuditEvents
18
+
19
+ permission_set_user_lookup :via_resource
20
+
21
+ # optional: as declared since 0.13 (the foreign key holds); stated, so the
22
+ # host's belongs_to_required_by_default decides nothing here.
23
+ belongs_to :resource, class_name: 'PermissionForge::Resource', optional: true
24
+
25
+ # One row per (resource, name): two rows would be OR-ed, so a second row
26
+ # could permit what the first one blocks. New installs also have a DB unique
27
+ # index (idx_pf_resource_actions_unique); the validation surfaces a 422
28
+ # instead of a RecordNotUnique 500. Checked when a row is created, renamed
29
+ # or moved, so duplicates that predate the index stay editable; the
30
+ # security audit lists them (`duplicate_actions`).
31
+ # Checked for a new row and on a rename only: a row whose name was
32
+ # blank before 0.14 stays editable in its other attributes.
33
+ validates :name, presence: { message: ->(*) { I18n.t('permission_forge.validations.name_blank') } },
34
+ if: -> { new_record? || will_save_change_to_name? }
35
+ validates :name, uniqueness: { scope: :resource_id,
36
+ message: ->(*) { I18n.t('permission_forge.errors.admin.action_taken') } },
37
+ if: -> { new_record? || will_save_change_to_name? || will_save_change_to_resource_id? }
38
+
39
+ # Active rows of active resources of active Permission Sets. An action with
40
+ # such a row is "managed": only people a set permits can run it.
41
+ scope :in_effect, lambda {
42
+ active.joins(resource: :permission_set)
43
+ .merge(PermissionForge::Resource.active).merge(PermissionForge::PermissionSet.active)
44
+ }
45
+
46
+ apply_tenant_scoping! # from the configuration, as the class loads (HasTenantScoping)
47
+
48
+ # For a loaded record (see .in_effect).
49
+ def in_effect?
50
+ is_active && resource.in_effect?
51
+ end
52
+
53
+ # @return [String] the label, or the humanized name when there is none
54
+ def display_name
55
+ label.presence || name.humanize
56
+ end
57
+ end
58
+ end