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,94 @@
1
+ module PermissionForge
2
+ # The audit log: every recorded event, filtered and paged, one event in
3
+ # full, and the exports (the filtered log as CSV or JSON, and who holds
4
+ # what). Read-only: nothing here writes an event — viewing and exporting
5
+ # are recorded by Audit::AuditedViews, a refusal by AuditLogGate.
6
+ #
7
+ # @api private
8
+ class AuditEventsController < ApplicationController
9
+ include PermissionForge::AuditLogGate
10
+
11
+ EXPORT_FORMATS = %w[csv json].freeze
12
+ MAX_KEY_LENGTH = 255
13
+
14
+ pf_exports EXPORT_FORMATS.map(&:to_sym), only: %i[index holdings]
15
+ before_action :pf_require_audit_log!
16
+
17
+ def index
18
+ @log = PermissionForge::Admin::AuditLog.new(params, include_views: export?)
19
+ return pf_stream(PermissionForge::Admin::Exports::Events.call(@log.relation, filters: @log.filters), 'audit-log') if export?
20
+
21
+ @page = @log.page(params[:page])
22
+ @lookups = PermissionForge::Admin::AuditLookups.new(@page.events)
23
+ rescue PermissionForge::Audit::EventQuery::Invalid => e
24
+ invalid_filters(e)
25
+ end
26
+
27
+ def show
28
+ @event = find_event || pf_not_found
29
+ @lookups = PermissionForge::Admin::AuditLookups.new([@event])
30
+ @detail = PermissionForge::Admin::AuditEventDetail.new(@event, @lookups)
31
+ end
32
+
33
+ # Who holds what: every assignment as it stands, with its review.
34
+ def holdings
35
+ return head(:not_acceptable) unless export?
36
+
37
+ pf_stream(PermissionForge::Admin::Exports::Holdings.call, 'who-holds-what')
38
+ end
39
+
40
+ private
41
+
42
+ def export?
43
+ EXPORT_FORMATS.include?(request.format.symbol.to_s)
44
+ end
45
+
46
+ # By uuid (what the pages link to), or by id. A key no column can hold
47
+ # (a NUL byte, longer than a column) is not looked for.
48
+ def find_event
49
+ key = params[:id].to_s
50
+ return nil if key.include?("\0") || key.length > MAX_KEY_LENGTH
51
+
52
+ events = PermissionForge::AuditEvent
53
+ events.find_by(uuid: key) || (key.match?(/\A\d{1,18}\z/) ? events.find_by(id: key) : nil)
54
+ end
55
+
56
+ def invalid_filters(error)
57
+ return head(:unprocessable_entity) if export?
58
+
59
+ @errors = error.errors
60
+ @page = nil
61
+ render :index, status: :unprocessable_entity
62
+ end
63
+
64
+ # Hands the file over as it is written (batch by batch), with no cap.
65
+ def pf_stream(document, name)
66
+ format = request.format.symbol.to_s
67
+ headers.merge!(pf_download_headers(format, PermissionForge::Admin::Exports.filename(name, format)))
68
+ self.response_body = pf_in_request_context(document.chunks(format))
69
+ end
70
+
71
+ # The body is written after the action's around_actions have ended: the
72
+ # record rules are no longer suspended (the people of the rows would be
73
+ # looked up under the admin's own rules) and the host's tenant may be gone.
74
+ # Each is captured here, in the action, and put back while the body is written.
75
+ def pf_in_request_context(chunks)
76
+ tenant = PermissionForge.ambient_tenant
77
+ Enumerator.new do |out|
78
+ PermissionForge.with_ambient_tenant(tenant) do
79
+ PermissionForge.without_permission_scope { chunks.each { |chunk| out << chunk } }
80
+ end
81
+ end
82
+ end
83
+
84
+ # Not buffered or cached on the way: no ETag digest (Last-Modified set),
85
+ # no proxy buffering, nothing stored.
86
+ def pf_download_headers(format, filename)
87
+ {
88
+ 'Content-Type' => Mime[format.to_sym].to_s,
89
+ 'Content-Disposition' => ActionDispatch::Http::ContentDisposition.format(disposition: 'attachment', filename: filename),
90
+ 'Cache-Control' => 'no-store', 'Last-Modified' => Time.current.httpdate, 'X-Accel-Buffering' => 'no'
91
+ }
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,108 @@
1
+ module PermissionForge
2
+ # Bulk assign (paste emails/names and/or pick people) and bulk revoke for
3
+ # one Permission Set. Both apply immediately, through the model callbacks
4
+ # (one audit event per person, one correlation id per action), after
5
+ # config.admin_assignment_authorization (AssignmentAuthorization).
6
+ #
7
+ # @api private
8
+ class BulkAssignmentsController < ApplicationController
9
+ include PermissionForge::AssignmentAuthorization
10
+ include PermissionForge::ActingAdmin
11
+
12
+ before_action :set_permission_set
13
+ # A locked set refuses what widens access (EditableLock.widening_lock):
14
+ # new holders of a granting set, removing holders of a Muting one.
15
+ before_action :widening_grant_refused?, only: %i[new preview create]
16
+ before_action :widening_revoke_refused?, only: %i[confirm_revoke revoke]
17
+
18
+ def new
19
+ load_form
20
+ end
21
+
22
+ # Shows who would be assigned, who already has it and what wasn't found.
23
+ def preview
24
+ load_form
25
+ @result = matcher_result
26
+ render :new
27
+ end
28
+
29
+ def create
30
+ load_form
31
+ @result = matcher_result
32
+ @expiry_error = pf_resolve_expiry
33
+ @nobody = @result.to_assign.empty?
34
+ return render(:new, status: :unprocessable_entity) if @expiry_error || @nobody
35
+ return if grant_refused?
36
+
37
+ outcome = assign(@result.to_assign)
38
+ redirect_to assignments_path,
39
+ notice: t('permission_forge.flash.bulk_assignments.assigned', count: outcome[:assigned], set: @permission_set.name)
40
+ end
41
+
42
+ def confirm_revoke
43
+ @people = selected_people
44
+ end
45
+
46
+ def revoke
47
+ ids = selected_people.map(&:id)
48
+ return if pf_assignment_refused?(@permission_set, ids, :revoke, assignments_path)
49
+
50
+ outcome = PermissionForge::Services::BulkPermissionSetAssignment.call(
51
+ permission_set: @permission_set, user_ids: ids, action: :revoke
52
+ )
53
+ redirect_to assignments_path,
54
+ notice: t('permission_forge.flash.bulk_assignments.revoked', count: outcome[:revoked], set: @permission_set.name)
55
+ end
56
+
57
+ private
58
+
59
+ def set_permission_set
60
+ @permission_set = PermissionForge::PermissionSet.find(params[:permission_set_id])
61
+ end
62
+
63
+ def grant_refused?
64
+ pf_assignment_refused?(@permission_set, @result.to_assign.map(&:id), :grant, assignments_path)
65
+ end
66
+
67
+ def assignments_path
68
+ permission_forge.permission_set_path(@permission_set, tab: 'assignments')
69
+ end
70
+
71
+ def widening_grant_refused?
72
+ pf_widening_refused?(pf_widening_lock(@permission_set, :grant), permission_forge.permission_set_path(@permission_set))
73
+ end
74
+
75
+ def widening_revoke_refused?
76
+ pf_widening_refused?(pf_widening_lock(@permission_set, :revoke), assignments_path)
77
+ end
78
+
79
+ def load_form
80
+ @text = params[:identifiers].to_s
81
+ @query = params[:q].to_s.strip
82
+ @picked_ids = Array(params[:user_ids]).compact_blank.map(&:to_s)
83
+ @people = people_directory.pick(query: @query, limit: 8)
84
+ pf_load_expiry
85
+ end
86
+
87
+ def assign(people)
88
+ PermissionForge::Services::BulkPermissionSetAssignment.call(
89
+ permission_set: @permission_set, user_ids: people.map(&:id), action: :assign,
90
+ created_by: pf_acting_admin_record, expires_at: @expires_at
91
+ )
92
+ end
93
+
94
+ def matcher_result
95
+ PermissionForge::Admin::BulkPeopleMatcher.new(directory: people_directory)
96
+ .call(text: @text, user_ids: @picked_ids, permission_set: @permission_set)
97
+ end
98
+
99
+ # One per request: the picker and the matcher share its resolved people.
100
+ def people_directory
101
+ @people_directory ||= PermissionForge::Admin::PeopleDirectory.new
102
+ end
103
+
104
+ def selected_people
105
+ people_directory.find_all(params[:user_ids])
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,38 @@
1
+ module PermissionForge
2
+ # "Who can see iban?" — pick a resource and a field, see its level in every
3
+ # Permission Set that includes the resource.
4
+ #
5
+ # @api private
6
+ class FieldLookupsController < ApplicationController
7
+ # Labels are computed here from the allow-listed resource name (via
8
+ # ModelCatalog, which only resolves real models) so the view never
9
+ # constantizes request input.
10
+ def show
11
+ @resource_names = PermissionForge::Resource.distinct.order(:name).pluck(:name)
12
+ @resource_name = @resource_names.include?(params[:resource]) ? params[:resource] : nil
13
+ model = @resource_name && PermissionForge::Admin::ModelCatalog.new.find(@resource_name)
14
+ @fields = @resource_name ? field_names(@resource_name, model) : []
15
+ @field_options = @fields.map { |name| [name, field_label(model, name)] }
16
+ load_field(model)
17
+ end
18
+
19
+ private
20
+
21
+ def load_field(model)
22
+ @field = @fields.include?(params[:field]) ? params[:field] : nil
23
+ @field_label = field_label(model, @field) if @field
24
+ @rows = @field ? PermissionForge::Admin::FieldAcrossSets.call(@resource_name, @field) : []
25
+ end
26
+
27
+ def field_names(resource_name, model)
28
+ configured = PermissionForge::ResourceField.joins(:resource)
29
+ .where(PermissionForge::Resource.table_name => { name: resource_name })
30
+ .distinct.pluck(:name)
31
+ (configured + (model ? model.managed_column_names + model.virtual_field_names : [])).uniq.sort
32
+ end
33
+
34
+ def field_label(model, name)
35
+ model&.klass&.human_attribute_name(name) || name.humanize
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,24 @@
1
+ module PermissionForge
2
+ # "Needs attention": what the owner should look at, plus quick lookups.
3
+ #
4
+ # @api private
5
+ class HomeController < ApplicationController
6
+ PRIVACY_PREVIEW = 5
7
+
8
+ def index
9
+ @attention = pf_attention
10
+ @first_run = !PermissionForge::PermissionSet.exists?
11
+ @show_all_privacy = params[:privacy] == 'all'
12
+ @cleanup_preview = params[:cleanup].present?
13
+ @people_with_access = live_assignments.distinct.count(:user_id)
14
+ @active_sets = PermissionForge::PermissionSet.active.count
15
+ end
16
+
17
+ private
18
+
19
+ def live_assignments
20
+ PermissionForge::PermissionSetUser.active_assignments
21
+ .where(permission_set_id: PermissionForge::PermissionSet.active.select(:id))
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,13 @@
1
+ module PermissionForge
2
+ # Housekeeping triggered from the home page.
3
+ #
4
+ # @api private
5
+ class MaintenanceController < ApplicationController
6
+ # Removes expired assignments (the dry run is shown on Home first).
7
+ def cleanup
8
+ result = PermissionForge::Services::ExpiredAssignmentCleaner.call
9
+ redirect_to permission_forge.root_path,
10
+ notice: t('permission_forge.flash.maintenance.cleaned', count: result[:removed])
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,190 @@
1
+ module PermissionForge
2
+ # People: everyone in the host app, what they can access, and why.
3
+ #
4
+ # @api private
5
+ class PeopleController < ApplicationController
6
+ include PermissionForge::CacheConfirmation
7
+ include PermissionForge::AssignmentAuthorization
8
+ include PermissionForge::AuditLogGate
9
+
10
+ PER_PAGE = 25
11
+ TABS = %w[access effective audit].freeze
12
+ # Records listed per type under "Changes made by" (the CSV has them all).
13
+ CHANGES_SHOWN = 100
14
+ TIMELINE_PER_PAGE = 20
15
+
16
+ pf_exports %i[csv], only: :show # the person's changes
17
+ before_action :set_person, except: %i[index]
18
+
19
+ def index
20
+ @query = params[:q].to_s.strip
21
+ @filter = filter_param
22
+ @page = directory.search(query: @query, filter: @filter, page: params[:page], per_page: PER_PAGE)
23
+ @counts = directory.counts(query: @query)
24
+ @assignments = assignments_for(@page.people.map(&:id))
25
+ end
26
+
27
+ def show
28
+ @tab = TABS.include?(params[:tab]) ? params[:tab] : 'access'
29
+ @assignments = @person_assignments
30
+ @groups = PermissionForge::PermissionSetGroupUser.where(user_id: @person.id).includes(:permission_set_group)
31
+ return pf_refuse_audit_tab unless pf_audit_tab_allowed?
32
+
33
+ send("load_#{@tab}_tab")
34
+ respond_to do |format|
35
+ format.html
36
+ format.csv { send_changes_csv }
37
+ end
38
+ end
39
+
40
+ # --- offboarding ------------------------------------------------------------
41
+
42
+ def confirm_remove_all
43
+ load_removable
44
+ @lost = lost_resource_labels(@removable)
45
+ end
46
+
47
+ # Each assignment is destroyed through its callbacks (one
48
+ # assignment.revoked event each, sharing one correlation id). Granting
49
+ # sets lose the person also when locked; a locked Muting Permission Set
50
+ # keeps them (removing them would give access back — EditableLock).
51
+ def remove_all
52
+ load_removable
53
+ sets = @removable.map(&:permission_set).uniq
54
+ path = permission_forge.person_path(@person)
55
+ return if pf_assignment_refused?(sets, @person.id, :revoke, path)
56
+
57
+ count = @removable.size
58
+ confirmed = cache_confirmed? { PermissionForge::Audit.correlate { @removable.each(&:destroy!) } }
59
+ redirect_to permission_forge.person_path(@person),
60
+ **cache_flash(confirmed, { notice: :removed_all, alert: :removed_all_cache_kept }, count: count, name: person_name)
61
+ end
62
+
63
+ def refresh_cache
64
+ confirmed = cache_confirmed? { @person.invalidate_permissions if @person.respond_to?(:invalidate_permissions) }
65
+ redirect_to permission_forge.person_path(@person, tab: pf_tab_param(TABS)),
66
+ **cache_flash(confirmed, { notice: :cache_refreshed, alert: :cache_refresh_failed }, name: person_name)
67
+ end
68
+
69
+ # --- consent withdrawal -------------------------------------------------------
70
+
71
+ def consent_preview
72
+ @flags = consent_flags
73
+ @flag = params[:flag].presence || @flags.first
74
+ @affected = @person_assignments.select { |assignment| @flag.present? && assignment.permission_set.requires_session_flag == @flag }
75
+ end
76
+
77
+ def withdraw_consent
78
+ flag = params[:flag].to_s
79
+ return redirect_to(permission_forge.person_path(@person)) unless consent_flags.include?(flag)
80
+
81
+ count, invalidated = consent_service(flag, dry_run: false).values_at(:revoked_sets, :invalidated)
82
+ # nothing revoked means nothing to clear: that is not a failure
83
+ redirect_to permission_forge.person_path(@person),
84
+ **cache_flash(count.zero? || invalidated, { notice: :consent_withdrawn, alert: :consent_withdrawn_cache_kept },
85
+ count: count, flag: flag)
86
+ end
87
+
88
+ # --- erasure ----------------------------------------------------------------------
89
+
90
+ def erasure_preview
91
+ @preview = erasure_service(dry_run: true)
92
+ end
93
+
94
+ def erase
95
+ if params[:confirmation].to_s.strip != person_name
96
+ @preview = erasure_service(dry_run: true)
97
+ @confirmation_error = true
98
+ return render :erasure_preview, status: :unprocessable_entity
99
+ end
100
+
101
+ result = erasure_service(dry_run: false)
102
+ # nil: erased inside a transaction of the host's, the cache follows at its commit
103
+ confirmed = result[:cache_cleared] != false
104
+ redirect_to permission_forge.people_path,
105
+ **cache_flash(confirmed, { notice: :erased, alert: :erased_cache_kept }, name: person_name)
106
+ end
107
+
108
+ private
109
+
110
+ def directory
111
+ @directory ||= PermissionForge::Admin::PeopleDirectory.new
112
+ end
113
+
114
+ def filter_param
115
+ filter = params[:filter]
116
+ PermissionForge::Admin::PeopleDirectory::FILTERS.include?(filter) ? filter : 'all'
117
+ end
118
+
119
+ def set_person
120
+ @person = directory.find(params[:id]) || pf_not_found
121
+ @person_assignments = PermissionForge::PermissionSetUser.where(user_id: @person.id)
122
+ .includes(:permission_set).order(:created_at).to_a
123
+ end
124
+
125
+ def person_name
126
+ PermissionForge::Admin::PersonLabel.new(@person).name
127
+ end
128
+
129
+ # @removable: the assignments "Remove all access" removes; @kept: those
130
+ # of locked Muting Permission Sets, where removing widens access.
131
+ def load_removable
132
+ @kept, @removable = @person_assignments.partition do |assignment|
133
+ pf_widening_lock(assignment.permission_set, :revoke)
134
+ end
135
+ end
136
+
137
+ def assignments_for(user_ids)
138
+ PermissionForge::PermissionSetUser.where(user_id: user_ids).includes(:permission_set).group_by(&:user_id)
139
+ end
140
+
141
+ def load_access_tab; end
142
+
143
+ def load_effective_tab
144
+ service = PermissionForge::Admin::EffectiveAccess.new(user: @person)
145
+ @effective = service.call
146
+ @conditional_sets = service.conditional_sets
147
+ @open = Array(params[:open]).map(&:to_s)
148
+ @why = params[:why].to_s
149
+ end
150
+
151
+ def load_audit_tab
152
+ @changes = audit.changes_by(@person.id, limit: CHANGES_SHOWN)
153
+ @changes_capped = audit.capped?
154
+ @timeline_log = PermissionForge::Admin::AuditLog.new({ views: params[:views] },
155
+ scope: PermissionForge::Admin::AuditLog.for_person(@person.id))
156
+ @timeline = @timeline_log.page(params[:page], per_page: TIMELINE_PER_PAGE)
157
+ @timeline_lookups = PermissionForge::Admin::AuditLookups.new(@timeline.events)
158
+ end
159
+
160
+ def audit
161
+ @audit ||= PermissionForge::Admin::AuditTrail.new
162
+ end
163
+
164
+ def send_changes_csv
165
+ entries = audit.changes_by(@person.id).values.flatten
166
+ csv = audit.to_csv(entries, describe: helpers.method(:pf_audit_describe), person: helpers.method(:pf_audit_people))
167
+ response.headers['Cache-Control'] = 'no-store' # names of people, like the log's exports
168
+ send_data csv, filename: "permission-forge-changes-#{@person.id}.csv", type: 'text/csv'
169
+ end
170
+
171
+ def lost_resource_labels(assignments)
172
+ assignments.map(&:permission_set).reject(&:is_muting)
173
+ .flat_map { |ps| ps.resources.select(&:is_active).map { |r| helpers.pf_resource_plural(r.name) } }.uniq
174
+ end
175
+
176
+ # Flags gating sets this person has, plus configured flags.
177
+ def consent_flags
178
+ flags = @person_assignments.map { |assignment| assignment.permission_set.requires_session_flag }.compact_blank
179
+ (flags + (PermissionForge.configuration.admin_session_flags || {}).keys.map(&:to_s)).uniq
180
+ end
181
+
182
+ def consent_service(flag, dry_run:)
183
+ PermissionForge::Services::ConsentWithdrawalHandler.call(user: @person, consent_flag: flag, dry_run: dry_run)
184
+ end
185
+
186
+ def erasure_service(dry_run:)
187
+ PermissionForge::Services::DataSubjectErasure.call(user: @person, dry_run: dry_run)
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,71 @@
1
+ module PermissionForge
2
+ # Loads the one-page Permission Set editor and its tabs (resources matrix,
3
+ # assignments, conditions, audit) for PermissionSetsController. Tabs are
4
+ # dispatched as load_<tab>_tab.
5
+ #
6
+ # @api private
7
+ module PermissionSetEditor
8
+ AUDIT_TIMELINE_PER_PAGE = 20
9
+
10
+ private
11
+
12
+ def load_editor
13
+ @resources = @permission_set.resources.includes(:resource_fields, :resource_actions, :permission_set_rules).order(:name).to_a
14
+ @groups = PermissionForge::PermissionSetGroup.order(:name)
15
+ load_impact
16
+ @assignment_count = @permission_set.permission_set_users.count
17
+ end
18
+
19
+ def load_impact
20
+ live = @permission_set.permission_set_users.active_assignments.includes(:user).order(:created_at)
21
+ @people_count = live.count
22
+ @impact_people = live.limit(3).map(&:user).compact
23
+ @lost = @permission_set.is_muting ? [] : @permission_set.resources.select(&:is_active).map { |r| helpers.pf_resource_plural(r.name) }
24
+ end
25
+
26
+ def load_resources_tab
27
+ @catalog = PermissionForge::Admin::ModelCatalog.new
28
+ end
29
+
30
+ def load_assignments_tab
31
+ @assignments = @permission_set.permission_set_users.includes(:user).order(:created_at).to_a
32
+ end
33
+
34
+ def load_conditions_tab
35
+ @ip_text ||= helpers.pf_ip_list(@permission_set.allowed_ips).join("\n")
36
+ configured = (PermissionForge.configuration.admin_session_flags || {}).keys.map(&:to_s)
37
+ used = PermissionForge::PermissionSet.where.not(requires_session_flag: [nil, '']).distinct.pluck(:requires_session_flag)
38
+ @flag_options = (configured + used).uniq
39
+ end
40
+
41
+ def load_audit_tab
42
+ load_audit_filters
43
+ audit = PermissionForge::Admin::AuditTrail.new
44
+ @editors = PermissionForge::Admin::AuditTrail.editors(audit.for_permission_set(@permission_set))
45
+ @entries = audit.for_permission_set(@permission_set, who: @who, type: @type, days: @days, sort: @sort)
46
+ load_audit_timeline unless request.format.csv?
47
+ end
48
+
49
+ # The set's events, newest first (the log filtered by this set).
50
+ def load_audit_timeline
51
+ @timeline_log = PermissionForge::Admin::AuditLog.new({ views: params[:views] },
52
+ scope: PermissionForge::Admin::AuditLog.for_permission_set(@permission_set))
53
+ @timeline = @timeline_log.page(params[:page], per_page: AUDIT_TIMELINE_PER_PAGE)
54
+ @timeline_lookups = PermissionForge::Admin::AuditLookups.new(@timeline.events)
55
+ end
56
+
57
+ def load_audit_filters
58
+ @who = params[:who].presence
59
+ @type = params[:type].presence
60
+ @days = params[:days].presence
61
+ @sort = params[:sort] == 'asc' ? 'asc' : 'desc'
62
+ end
63
+
64
+ def send_audit_csv
65
+ audit = PermissionForge::Admin::AuditTrail.new
66
+ csv = audit.to_csv(@entries, describe: helpers.method(:pf_audit_describe), person: helpers.method(:pf_audit_people))
67
+ response.headers['Cache-Control'] = 'no-store' # names of people, like the log's exports
68
+ send_data csv, filename: "permission-forge-#{@permission_set.name.parameterize}-audit.csv", type: 'text/csv'
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,57 @@
1
+ module PermissionForge
2
+ # Group membership. Organisational only: it never gives Permission Sets.
3
+ # Adding and removing go through the model callbacks (group_membership.*
4
+ # audit events) after config.admin_assignment_authorization.
5
+ #
6
+ # @api private
7
+ class PermissionSetGroupUsersController < ApplicationController
8
+ include PermissionForge::AssignmentAuthorization
9
+
10
+ before_action :set_group
11
+ before_action :reject_locked
12
+
13
+ def create
14
+ user = member_user
15
+ return redirect_to(group_path, alert: t('permission_forge.flash.permission_set_group_users.user_not_found')) unless user
16
+ return if pf_assignment_refused?(@group, user.id, :add_member, group_path)
17
+
18
+ membership = PermissionForge::PermissionSetGroupUser.new(permission_set_group: @group, user: user)
19
+ return redirect_to(group_path, alert: t('permission_forge.flash.permission_set_group_users.add_failed')) unless membership.save
20
+
21
+ redirect_to group_path, notice: t('permission_forge.flash.groups.member_added', name: person_name(user))
22
+ end
23
+
24
+ # The member must be somebody config.user_list_resolver lists (404 otherwise).
25
+ def destroy
26
+ membership = @group.permission_set_group_users.find(params[:id])
27
+ PermissionForge::Admin::PeopleDirectory.new.find(membership.user_id) || pf_not_found
28
+ return if pf_assignment_refused?(@group, membership.user_id, :remove_member, group_path)
29
+
30
+ membership.destroy!
31
+ redirect_to group_path, notice: t('permission_forge.flash.permission_set_group_users.removed')
32
+ end
33
+
34
+ private
35
+
36
+ def set_group
37
+ @group = PermissionForge::PermissionSetGroup.find(params[:group_id])
38
+ end
39
+
40
+ # A locked group's members are frozen.
41
+ def reject_locked
42
+ pf_reject_locked!(@group, group_path)
43
+ end
44
+
45
+ def member_user
46
+ PermissionForge::Admin::PeopleDirectory.new.find(params[:user_id])
47
+ end
48
+
49
+ def person_name(user)
50
+ PermissionForge::Admin::PersonLabel.new(user).name
51
+ end
52
+
53
+ def group_path
54
+ permission_forge.group_path(@group)
55
+ end
56
+ end
57
+ end