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,111 @@
1
+ module PermissionForge
2
+ # Permission Set Groups: purely organisational. Membership never grants
3
+ # access; changes here save immediately and never change anyone's access.
4
+ #
5
+ # @api private
6
+ class PermissionSetGroupsController < ApplicationController
7
+ before_action :set_group, except: %i[index create]
8
+ before_action :reject_locked, only: %i[update destroy confirm_destroy add_set remove_set]
9
+
10
+ def index
11
+ load_index
12
+ @new_open = params[:new].present?
13
+ end
14
+
15
+ def create
16
+ @group = PermissionForge::PermissionSetGroup.new(name: pf_text_param(:name), is_active: true, is_editable: true)
17
+ return render_blank_name if @group.name.blank?
18
+
19
+ @group.save!
20
+ redirect_to permission_forge.group_path(@group), notice: t('permission_forge.flash.groups.created', name: @group.name)
21
+ end
22
+
23
+ def show
24
+ @sets = @group.permission_sets.order(:name)
25
+ @other_sets = PermissionForge::PermissionSet.where.not(id: @sets.select(:id)).includes(:permission_set_group).order(:name)
26
+ @members = @group.permission_set_group_users.includes(:user).order(:created_at)
27
+ @candidates = PermissionForge.configuration.assignable_users(exclude_ids: @members.map(&:user_id))
28
+ @people_counts = PermissionForge::PermissionSetUser.active_assignments.group(:permission_set_id).count
29
+ end
30
+
31
+ def update
32
+ attrs = group_params
33
+ return redirect_to(permission_forge.group_path(@group), alert: t('permission_forge.errors.admin.group_blank')) if blank_name?(attrs)
34
+
35
+ @group.update!(attrs)
36
+ redirect_to permission_forge.group_path(@group), notice: t('permission_forge.flash.groups.updated')
37
+ end
38
+
39
+ def duplicate
40
+ copy = PermissionForge::Services::PermissionSetGroupCloner.call(
41
+ permission_set_group: @group, attrs: { name: t('permission_forge.permission_sets.copy_name', name: @group.name), is_editable: true },
42
+ unlock: true
43
+ )
44
+ redirect_to permission_forge.group_path(copy), notice: t('permission_forge.flash.groups.duplicated', name: @group.name)
45
+ end
46
+
47
+ def confirm_destroy; end
48
+
49
+ def destroy
50
+ name = @group.name
51
+ @group.destroy!
52
+ redirect_to permission_forge.groups_path, notice: t('permission_forge.flash.groups.deleted', name: name)
53
+ end
54
+
55
+ # Moving a set here also changes the list of the group it is in now: a
56
+ # locked one keeps it (EditableLock.locked_group_for_move), as in the
57
+ # set editor and the API.
58
+ def add_set
59
+ set = PermissionForge::PermissionSet.find(params[:permission_set_id])
60
+ leaving = PermissionForge::Admin::EditableLock.locked_group_for_move(from: set.permission_set_group_id, to: @group.id)
61
+ return if pf_reject_locked!([set, leaving], permission_forge.group_path(@group))
62
+
63
+ set.update!(permission_set_group: @group)
64
+ redirect_to permission_forge.group_path(@group), notice: t('permission_forge.flash.groups.set_added', set: set.name)
65
+ end
66
+
67
+ def remove_set
68
+ set = @group.permission_sets.find(params[:permission_set_id])
69
+ return if pf_reject_locked!(set, permission_forge.group_path(@group))
70
+
71
+ set.update!(permission_set_group: nil)
72
+ redirect_to permission_forge.group_path(@group), notice: t('permission_forge.flash.groups.set_removed', set: set.name)
73
+ end
74
+
75
+ private
76
+
77
+ def set_group
78
+ @group = PermissionForge::PermissionSetGroup.find(params[:id])
79
+ end
80
+
81
+ def load_index
82
+ @groups = PermissionForge::PermissionSetGroup.includes(:permission_sets, :permission_set_group_users).order(:name)
83
+ @ungrouped = PermissionForge::PermissionSet.where(permission_set_group_id: nil).order(:name)
84
+ end
85
+
86
+ # Re-renders the index with the "new group" form open and an error.
87
+ def render_blank_name
88
+ @new_error = t('permission_forge.errors.admin.group_blank')
89
+ @new_open = true
90
+ load_index
91
+ render(:index, status: :unprocessable_entity)
92
+ end
93
+
94
+ def group_params
95
+ pf_require_hash(:group).permit(:name, :description, :is_active).tap { |attrs| strip_name!(attrs) }
96
+ end
97
+
98
+ def strip_name!(attrs)
99
+ attrs[:name] = attrs[:name].to_s.strip if attrs.key?(:name)
100
+ end
101
+
102
+ # A name was submitted but is empty.
103
+ def blank_name?(attrs)
104
+ attrs.key?(:name) && attrs[:name].blank?
105
+ end
106
+
107
+ def reject_locked
108
+ pf_reject_locked!(@group, permission_forge.group_path(@group))
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,97 @@
1
+ module PermissionForge
2
+ # Record rules (row-level filters) for a Resource, built as sentences.
3
+ #
4
+ # @api private
5
+ class PermissionSetRulesController < ApplicationController
6
+ before_action :set_resource
7
+ before_action :set_rule, only: %i[edit update destroy confirm_destroy toggle]
8
+ before_action :reject_locked
9
+ helper_method :rule_builder
10
+
11
+ def new
12
+ @form = PermissionForge::Admin::RuleForm.new(resource: @resource)
13
+ end
14
+
15
+ def create
16
+ @form = PermissionForge::Admin::RuleForm.new(resource: @resource, params: rule_params)
17
+ @rule = @resource.permission_set_rules.build(permission_set: @permission_set)
18
+ save_rule(:new, 'created')
19
+ end
20
+
21
+ def edit
22
+ @form = PermissionForge::Admin::RuleForm.new(resource: @resource, rule: @rule)
23
+ end
24
+
25
+ def update
26
+ @form = PermissionForge::Admin::RuleForm.new(resource: @resource, params: rule_params)
27
+ save_rule(:edit, 'updated')
28
+ end
29
+
30
+ # Re-renders the builder after a relationship/field/operator change.
31
+ def refresh
32
+ @form = PermissionForge::Admin::RuleForm.new(resource: @resource, params: rule_params)
33
+ @rule = @resource.permission_set_rules.find_by(id: params[:rule_id])
34
+ render(@rule ? :edit : :new)
35
+ end
36
+
37
+ def toggle
38
+ @rule.update!(is_active: !@rule.is_active)
39
+ key = @rule.is_active ? 'turned_on' : 'turned_off'
40
+ redirect_to rules_path, notice: t("permission_forge.flash.rules.#{key}", rule: @rule.name)
41
+ end
42
+
43
+ def confirm_destroy; end
44
+
45
+ def destroy
46
+ name = @rule.name
47
+ @rule.destroy!
48
+ redirect_to rules_path, notice: t('permission_forge.flash.rules.deleted', rule: name)
49
+ end
50
+
51
+ private
52
+
53
+ # What the builder page draws: built here, read by the template.
54
+ def rule_builder
55
+ @rule_builder ||= PermissionForge::Admin::RuleBuilder.new(form: @form, resource: @resource, model_errors: @model_errors)
56
+ end
57
+
58
+ def set_resource
59
+ @permission_set = PermissionForge::PermissionSet.find(params[:permission_set_id])
60
+ @resource = @permission_set.resources.find_by!(name: params[:resource_model])
61
+ end
62
+
63
+ def set_rule
64
+ @rule = @resource.permission_set_rules.find(params[:id])
65
+ end
66
+
67
+ def reject_locked
68
+ locked = [@permission_set, @resource, @rule].compact.find { |r| pf_locked?(r) }
69
+ pf_reject_locked!(locked, rules_path) if locked
70
+ end
71
+
72
+ def rules_path
73
+ permission_forge.permission_set_resource_path(@permission_set, @resource.name, tab: 'rules')
74
+ end
75
+
76
+ def rule_params
77
+ pf_hash_param(:rule).permit(:name, :field_name, :operator, :value, :values_text, :value_from, :value_to,
78
+ :current_user, :include_nulls, :failure_message, :join, :is_active, :remove_step,
79
+ path: [], values: [])
80
+ end
81
+
82
+ def save_rule(template, flash_key)
83
+ unless @form.valid?
84
+ @people_count = pf_people_count(@permission_set)
85
+ return render(template, status: :unprocessable_entity)
86
+ end
87
+
88
+ @rule.assign_attributes(@form.attributes)
89
+ if @rule.save
90
+ redirect_to rules_path, notice: t("permission_forge.flash.rules.#{flash_key}", rule: @rule.name)
91
+ else
92
+ @model_errors = @rule.errors.full_messages
93
+ render template, status: :unprocessable_entity
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,227 @@
1
+ module PermissionForge
2
+ # Permission Sets: list, create (granting or muting), and the one-page
3
+ # editor (resources matrix, assignments, conditions, audit).
4
+ #
5
+ # @api private
6
+ class PermissionSetsController < ApplicationController
7
+ include PermissionSetEditor
8
+ include PermissionForge::AuditLogGate
9
+
10
+ FILTERS = %w[all granting muting conditional off].freeze
11
+ TABS = %w[resources assignments conditions audit].freeze
12
+
13
+ # List filter => predicate on a set ('all' and unknown filters match everything).
14
+ FILTER_MATCHERS = {
15
+ 'granting' => ->(set) { !set.is_muting },
16
+ 'muting' => lambda(&:is_muting),
17
+ 'conditional' => lambda(&:conditional?),
18
+ 'off' => ->(set) { !set.is_active }
19
+ }.freeze
20
+
21
+ pf_exports %i[csv], only: :show # the audit tab
22
+ before_action :set_permission_set, except: %i[index new create]
23
+ before_action :reject_locked, only: %i[review apply destroy confirm_destroy switch_mode confirm_switch_mode conditions]
24
+ before_action :reject_locked_group_move, only: %i[create review apply]
25
+
26
+ def index
27
+ @query = params[:q].to_s.strip
28
+ @filter = filter_param
29
+ all = PermissionForge::PermissionSet.includes(:permission_set_group, :resources).order(:name).to_a
30
+ @total = all.size
31
+ @counts = filter_counts(all)
32
+ @permission_sets = all.select { |ps| filter_match?(ps, @filter) && query_match?(ps) }
33
+ @people_counts = people_counts
34
+ end
35
+
36
+ def new
37
+ @muting = params[:type] == 'muting'
38
+ @name = params[:name].to_s
39
+ load_new_form
40
+ end
41
+
42
+ def create
43
+ @muting = params[:type] == 'muting'
44
+ @name = pf_text_param(:name)
45
+ load_new_form
46
+ @error = name_error(@name)
47
+ return render(:new, status: :unprocessable_entity) if @error
48
+
49
+ set = build_set
50
+ redirect_to permission_forge.permission_set_path(set), notice: t('permission_forge.flash.permission_sets.created', name: set.name)
51
+ end
52
+
53
+ def show
54
+ # The CSV is the audit tab's export, whatever tab the link came from.
55
+ @tab = if request.format.csv?
56
+ 'audit'
57
+ else
58
+ (TABS.include?(params[:tab]) ? params[:tab] : 'resources')
59
+ end
60
+ load_editor
61
+ return pf_refuse_audit_tab unless pf_audit_tab_allowed?
62
+
63
+ send("load_#{@tab}_tab")
64
+ respond_to do |format|
65
+ format.html
66
+ format.csv { send_audit_csv }
67
+ end
68
+ end
69
+
70
+ # "Review and save": lists every pending change and who it affects.
71
+ def review
72
+ @change_set = change_set
73
+ if @change_set.empty?
74
+ return redirect_to(pf_back_or(permission_forge.permission_set_path(@permission_set)),
75
+ notice: t('permission_forge.flash.permission_sets.no_changes'))
76
+ end
77
+
78
+ load_editor
79
+ @changes = @change_set.changes
80
+ render :review
81
+ end
82
+
83
+ def apply
84
+ changes = change_set.apply!
85
+ redirect_to back_to_set, notice: t('permission_forge.flash.permission_sets.saved',
86
+ count: changes.size, people: helpers.pf_people_text(pf_people_count(@permission_set)))
87
+ rescue PermissionForge::Admin::ChangeSet::Invalid => e
88
+ redirect_to back_to_set, alert: t('permission_forge.flash.permission_sets.save_failed', errors: e.message)
89
+ end
90
+
91
+ def duplicate
92
+ name = PermissionForge::Admin::PermissionSetFactory.unique_name(
93
+ t('permission_forge.permission_sets.copy_name', name: @permission_set.name)
94
+ )
95
+ copy = PermissionForge::Admin::PermissionSetFactory.duplicate(@permission_set, name: name)
96
+ group = @permission_set.permission_set_group
97
+ notice = if group && copy.permission_set_group_id.nil?
98
+ t('permission_forge.flash.permission_sets.duplicated_outside_group', name: @permission_set.name, group: group.name)
99
+ else
100
+ t('permission_forge.flash.permission_sets.duplicated', name: @permission_set.name)
101
+ end
102
+ redirect_to permission_forge.permission_set_path(copy), notice: notice
103
+ end
104
+
105
+ def confirm_destroy
106
+ load_impact
107
+ end
108
+
109
+ def destroy
110
+ name = @permission_set.name
111
+ @permission_set.destroy!
112
+ redirect_to permission_forge.permission_sets_path, notice: t('permission_forge.flash.permission_sets.deleted', name: name)
113
+ end
114
+
115
+ def confirm_switch_mode
116
+ load_impact
117
+ end
118
+
119
+ # Granting <-> muting. Field flags are kept; their meaning inverts.
120
+ def switch_mode
121
+ @permission_set.update!(is_muting: !@permission_set.is_muting)
122
+ key = @permission_set.is_muting ? 'switched_muting' : 'switched_granting'
123
+ redirect_to permission_forge.permission_set_path(@permission_set),
124
+ notice: t("permission_forge.flash.permission_sets.#{key}", name: @permission_set.name)
125
+ end
126
+
127
+ def conditions
128
+ @conditions_form = PermissionForge::Admin::ConditionsForm.new(params) # the re-render reads it back
129
+ attrs = @conditions_form.attributes
130
+ @condition_errors = @conditions_form.errors
131
+ if @condition_errors.empty? && @permission_set.update(attrs)
132
+ return redirect_to permission_forge.permission_set_path(@permission_set, tab: 'conditions'),
133
+ notice: t('permission_forge.flash.permission_sets.conditions_saved')
134
+ end
135
+
136
+ @tab = 'conditions'
137
+ @ip_text = params[:allowed_ips].to_s
138
+ load_editor
139
+ load_conditions_tab
140
+ render :show, status: :unprocessable_entity
141
+ end
142
+
143
+ private
144
+
145
+ def set_permission_set
146
+ @permission_set = PermissionForge::PermissionSet.find(params[:id])
147
+ end
148
+
149
+ # Deleting the set also changes its group's list of sets: a locked group
150
+ # refuses it (EditableLock.container_of).
151
+ def reject_locked
152
+ container = PermissionForge::Admin::EditableLock.container_of(@permission_set) if action_name.include?('destroy')
153
+ pf_reject_locked!([@permission_set, container], permission_forge.permission_set_path(@permission_set))
154
+ end
155
+
156
+ # A set created in, or moved into or out of, a locked Permission Set
157
+ # Group changes that group's list of sets: refused, as on the group page.
158
+ def reject_locked_group_move
159
+ requested = @permission_set ? pf_hash_param(:pf)[:set] : params
160
+ return unless requested.respond_to?(:key?) && requested.key?(:permission_set_group_id)
161
+
162
+ locked = PermissionForge::Admin::EditableLock.locked_group_for_move(
163
+ from: @permission_set&.permission_set_group_id, to: requested[:permission_set_group_id]
164
+ )
165
+ back = @permission_set ? permission_forge.permission_set_path(@permission_set) : permission_forge.permission_sets_path
166
+ pf_reject_locked!(locked, back) if locked
167
+ end
168
+
169
+ def back_to_set
170
+ pf_back_or(permission_forge.permission_set_path(@permission_set))
171
+ end
172
+
173
+ def change_set
174
+ PermissionForge::Admin::ChangeSet.new(permission_set: @permission_set, params: pf_hash_param(:pf))
175
+ end
176
+
177
+ # --- list -----------------------------------------------------------------
178
+
179
+ def filter_param
180
+ filter = params[:filter]
181
+ FILTERS.include?(filter) ? filter : 'all'
182
+ end
183
+
184
+ def filter_counts(sets)
185
+ FILTERS.index_with { |filter| sets.count { |ps| filter_match?(ps, filter) } }
186
+ end
187
+
188
+ def filter_match?(set, filter)
189
+ matcher = FILTER_MATCHERS[filter]
190
+ matcher ? matcher.call(set) : true
191
+ end
192
+
193
+ def query_match?(set)
194
+ return true if @query.empty?
195
+
196
+ needle = @query.downcase
197
+ [set.name, set.description].compact.any? { |v| v.downcase.include?(needle) }
198
+ end
199
+
200
+ def people_counts
201
+ PermissionForge::PermissionSetUser.active_assignments.group(:permission_set_id).count
202
+ end
203
+
204
+ # --- create / duplicate -----------------------------------------------------
205
+
206
+ def load_new_form
207
+ @groups = PermissionForge::PermissionSetGroup.order(:name)
208
+ @sources = PermissionForge::PermissionSet.order(:name).to_a
209
+ @start_from = params[@muting ? :start_from_muting : :start_from_granting].to_s
210
+ @group_id = params[:permission_set_group_id].to_s
211
+ end
212
+
213
+ def name_error(name)
214
+ return t('permission_forge.errors.admin.set_name_blank') if name.blank?
215
+ return t('permission_forge.errors.admin.set_name_taken') if PermissionForge::Admin::PermissionSetFactory.name_taken?(name)
216
+
217
+ nil
218
+ end
219
+
220
+ def build_set
221
+ group = PermissionForge::PermissionSetGroup.find_by(id: @group_id)
222
+ source = @sources.find { |ps| ps.id.to_s == @start_from && ps.is_muting == @muting }
223
+ PermissionForge::Admin::PermissionSetFactory.create({ name: @name, is_muting: @muting, permission_set_group: group },
224
+ source: source)
225
+ end
226
+ end
227
+ end
@@ -0,0 +1,26 @@
1
+ module PermissionForge
2
+ # Per-browser admin preferences, stored in cookies scoped to the engine's
3
+ # mount path: colour theme and dismissed helper banners.
4
+ #
5
+ # @api private
6
+ class PreferencesController < ApplicationController
7
+ HELP_KEY = /\A[a-z_]{1,40}\z/
8
+
9
+ def update
10
+ theme = params[:theme]
11
+ pf_cookie(:pf_theme, theme) if THEMES.include?(theme.to_s)
12
+ dismiss_help(params[:dismiss_help].to_s)
13
+ redirect_to pf_back_or(permission_forge.root_path)
14
+ end
15
+
16
+ private
17
+
18
+ # Blank or malformed keys are ignored.
19
+ def dismiss_help(key)
20
+ return unless key.match?(HELP_KEY)
21
+
22
+ keys = (pf_dismissed_help + [key]).uniq.last(30)
23
+ pf_cookie(:pf_help, keys.join(','))
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,106 @@
1
+ module PermissionForge
2
+ # Special actions on a Resource (e.g. export, sync). Saves apply immediately.
3
+ # With controller_action_discovery on, the drawers also show where an action
4
+ # runs and what its row means; discovered actions added from the Actions tab
5
+ # go through Admin::ChangeSet instead (review, then apply).
6
+ #
7
+ # @api private
8
+ class ResourceActionsController < ApplicationController
9
+ before_action :set_resource
10
+ before_action :set_action, only: %i[show update destroy confirm_destroy]
11
+ before_action :reject_locked, only: %i[new create update destroy confirm_destroy]
12
+
13
+ # `?key=export` opens the form for a discovered controller action.
14
+ def new
15
+ @action_record = @resource.resource_actions.build(name: suggested_key, is_active: true,
16
+ is_permitted: !@permission_set.is_muting)
17
+ load_rows
18
+ end
19
+
20
+ def create
21
+ @action_record = @resource.resource_actions.build(action_params)
22
+ normalize_name!(@action_record)
23
+ @errors = validate(@action_record)
24
+ return render_invalid if @errors.any?
25
+
26
+ @action_record.save!
27
+ redirect_to resource_path, notice: t('permission_forge.flash.actions.created', action: @action_record.display_name)
28
+ end
29
+
30
+ def show
31
+ load_rows
32
+ @across = @action_rows.across_sets(@action_record.name)
33
+ @people_count = pf_people_count(@permission_set)
34
+ end
35
+
36
+ def update
37
+ @action_record.assign_attributes(action_params.except(:name))
38
+ @action_record.save!
39
+ redirect_to pf_back_or(resource_path), notice: t('permission_forge.flash.actions.updated', action: @action_record.display_name)
40
+ end
41
+
42
+ def confirm_destroy; end
43
+
44
+ def destroy
45
+ name = @action_record.display_name
46
+ @action_record.destroy!
47
+ redirect_to resource_path, notice: t('permission_forge.flash.actions.deleted', action: name)
48
+ end
49
+
50
+ private
51
+
52
+ def set_resource
53
+ @permission_set = PermissionForge::PermissionSet.find(params[:permission_set_id])
54
+ @resource = @permission_set.resources.find_by!(name: params[:resource_model])
55
+ end
56
+
57
+ def set_action
58
+ @action_record = @resource.resource_actions.find(params[:id])
59
+ end
60
+
61
+ def reject_locked
62
+ locked = [@permission_set, @resource].find { |r| pf_locked?(r) }
63
+ pf_reject_locked!(locked, resource_path) if locked
64
+ end
65
+
66
+ def resource_path
67
+ permission_forge.permission_set_resource_path(@permission_set, @resource.name, tab: 'actions')
68
+ end
69
+
70
+ def action_params
71
+ pf_require_hash(:resource_action).permit(:name, :label, :description, :is_permitted, :is_active)
72
+ end
73
+
74
+ # "Export CSV " => "export_csv"
75
+ def normalize_name!(record)
76
+ record.name = record.name.to_s.strip.downcase.tr(' ', '_')
77
+ end
78
+
79
+ # Rows for the drawers: endpoints, guard status and effect. @row is the
80
+ # row of the action in the form (nil for a key discovery does not know).
81
+ def load_rows
82
+ @action_rows = PermissionForge::Admin::ActionRows.new(@resource)
83
+ @row = @action_rows.row(@action_record.name)
84
+ end
85
+
86
+ def render_invalid
87
+ load_rows
88
+ render :new, status: :unprocessable_entity
89
+ end
90
+
91
+ def suggested_key
92
+ key = params[:key].to_s
93
+ key if key.match?(PermissionForge::ControllerActions::KEY_FORMAT)
94
+ end
95
+
96
+ def validate(record)
97
+ errors = {}
98
+ if !record.name.to_s.match?(PermissionForge::ControllerActions::KEY_FORMAT)
99
+ errors[:name] = t('permission_forge.errors.admin.action_format')
100
+ elsif @resource.resource_actions.where(name: record.name).exists?
101
+ errors[:name] = t('permission_forge.errors.admin.action_taken')
102
+ end
103
+ errors
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,129 @@
1
+ module PermissionForge
2
+ # Field detail (drawer or page) for one column of a Resource — configured or
3
+ # not yet configured. Saves apply immediately (with an impact line).
4
+ #
5
+ # @api private
6
+ class ResourceFieldsController < ApplicationController
7
+ include PermissionForge::ActingAdmin
8
+
9
+ before_action :load_field
10
+ before_action :reject_locked, only: %i[update acknowledge apply_recommendation]
11
+
12
+ def show
13
+ row = @rows.row(@key)
14
+ @detail = PermissionForge::Admin::FieldDetail.new(
15
+ resource: @resource, row: row, rows: @rows,
16
+ rules: field_rules, across: PermissionForge::Admin::FieldAcrossSets.call(@resource.name, @key),
17
+ people_count: pf_people_count(@permission_set), explain_people: explain_candidates,
18
+ explain: (explain(params[:explain_user_id]) if params[:explain_user_id].present?),
19
+ explain_user_id: params[:explain_user_id]
20
+ )
21
+ end
22
+
23
+ def update
24
+ attrs = pf_require_hash(:field).permit(:level, :mask_strategy, :label, :description, :is_active).to_h
25
+ changes = apply_field_changes(attrs)
26
+ finish_new_field(attrs) unless @field
27
+ redirect_to pf_back_or(resource_path), notice: saved_notice(changes)
28
+ rescue PermissionForge::Admin::ChangeSet::Invalid => e
29
+ redirect_to field_path, alert: t('permission_forge.flash.permission_sets.save_failed', errors: e.message)
30
+ end
31
+
32
+ # Privacy audit: "This is intended" — stop flagging this unmasked field.
33
+ # Records who acknowledged it (sensitive_acknowledged_by_id, when the
34
+ # column exists) through a normal update: one field.sensitive_acknowledged event.
35
+ def acknowledge
36
+ if @field && PermissionForge::Admin::AttentionItems.acknowledgement_supported?
37
+ attrs = { sensitive_acknowledged_at: Time.current }
38
+ attrs[:sensitive_acknowledged_by_id] = pf_acting_admin_record&.id if @field.has_attribute?(:sensitive_acknowledged_by_id)
39
+ @field.audit_as('field.sensitive_acknowledged') { @field.update!(attrs) }
40
+ end
41
+ redirect_to pf_back_or(field_path), notice: t('permission_forge.flash.fields.acknowledged', field: @key)
42
+ end
43
+
44
+ # Applies the recommended mask for a sensitive column.
45
+ def apply_recommendation
46
+ match = PermissionForge::Admin::SensitiveColumns.new.match(@key)
47
+ return redirect_to(field_path) unless match
48
+
49
+ level = @permission_set.is_muting ? 'mask' : 'masked'
50
+ apply_field_changes('level' => level, 'mask_strategy' => match.strategy)
51
+ redirect_to pf_back_or(field_path), notice: t('permission_forge.flash.fields.recommendation_applied', field: @key)
52
+ end
53
+
54
+ private
55
+
56
+ def load_field
57
+ load_resource
58
+ @key = params[:key].to_s
59
+ @field = @resource.resource_fields.find { |field| field.name == @key }
60
+ @rows = PermissionForge::Admin::FieldRows.new(@resource)
61
+ pf_not_found unless @field || @rows.row(@key)
62
+ end
63
+
64
+ def load_resource
65
+ @permission_set = PermissionForge::PermissionSet.find(params[:permission_set_id])
66
+ @resource = @permission_set.resources.includes(:resource_fields, :permission_set_rules).find_by!(name: params[:resource_model])
67
+ end
68
+
69
+ # Applies `attrs` to this field through a ChangeSet; an unconfigured column
70
+ # is created at attrs['level'].
71
+ def apply_field_changes(attrs)
72
+ change_params = if @field
73
+ { fields: { @field.id.to_s => attrs } }
74
+ else
75
+ { new_fields: { @resource.id.to_s => { @key => { 'level' => attrs['level'] } } } }
76
+ end
77
+ PermissionForge::Admin::ChangeSet.new(permission_set: @permission_set, params: change_params).apply!
78
+ end
79
+
80
+ # Filter rules (no relationship) on this field.
81
+ def field_rules
82
+ @resource.permission_set_rules.select { |rule| rule.field_name == @key && rule.field_accessor_path.blank? }
83
+ end
84
+
85
+ def reject_locked
86
+ locked = [@permission_set, @resource, @field].compact.find { |r| pf_locked?(r) }
87
+ pf_reject_locked!(locked, field_path) if locked
88
+ end
89
+
90
+ def resource_path
91
+ permission_forge.permission_set_resource_path(@permission_set, @resource.name)
92
+ end
93
+
94
+ def field_path
95
+ permission_forge.permission_set_resource_field_path(@permission_set, @resource.name, @key)
96
+ end
97
+
98
+ def finish_new_field(attrs)
99
+ field = @resource.resource_fields.reload.find { |candidate| candidate.name == @key }
100
+ return unless field
101
+
102
+ extra = attrs.slice('label', 'description').compact_blank
103
+ extra['mask_strategy'] = attrs['mask_strategy'] if attrs['mask_strategy'].present? && field.mask_access
104
+ field.update!(extra) if extra.any?
105
+ end
106
+
107
+ def saved_notice(changes)
108
+ return t('permission_forge.flash.permission_sets.no_changes') if changes.empty?
109
+
110
+ t('permission_forge.flash.permission_sets.saved', count: changes.size,
111
+ people: helpers.pf_people_text(pf_people_count(@permission_set)))
112
+ end
113
+
114
+ def explain_candidates
115
+ set_ids = PermissionForge::Resource.where(name: @resource.name).select(:permission_set_id)
116
+ user_ids = PermissionForge::PermissionSetUser.active_assignments.where(permission_set_id: set_ids)
117
+ .distinct.limit(200).pluck(:user_id)
118
+ PermissionForge::Admin::PeopleDirectory.new.find_all(user_ids)
119
+ end
120
+
121
+ def explain(user_id)
122
+ user = PermissionForge::Admin::PeopleDirectory.new.find(user_id)
123
+ return nil unless user
124
+
125
+ outcome = PermissionForge::Admin::EffectiveAccess.call(user: user).find { |r| r.name == @resource.name }
126
+ { user: user, field: outcome&.fields&.find { |field| field.name == @key } }
127
+ end
128
+ end
129
+ end