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,69 @@
1
+ module PermissionForge
2
+ # Column-level read, edit and mask access to one field of a resource.
3
+ #
4
+ # @api public
5
+ class ResourceField < ActiveRecord::Base
6
+ # The table name, with the configured prefix (read on every call, so a prefix set at runtime applies).
7
+ def self.table_name = "#{PermissionForge.table_name_prefix}resource_fields"
8
+
9
+ include PermissionForge::Concerns::HasActiveScope
10
+ include PermissionForge::Concerns::HasUuid
11
+ include PermissionForge::Concerns::ValidatesColumnLengths
12
+ include PermissionForge::Concerns::HasAuditTrail
13
+ include PermissionForge::Concerns::HasTenantScoping
14
+ include PermissionForge::Concerns::HasPermissionSetUsers
15
+ include PermissionForge::Concerns::ValidatesDependency
16
+ include PermissionForge::Concerns::PermissionsRegenerator
17
+ include PermissionForge::Concerns::RecordsAuditEvents
18
+
19
+ permission_set_user_lookup :via_resource
20
+
21
+ # optional: as declared since 0.13 (the foreign key holds); stated, so the
22
+ # host's belongs_to_required_by_default decides nothing here.
23
+ belongs_to :resource, class_name: 'PermissionForge::Resource', optional: true
24
+
25
+ # Active rows of active resources of active Permission Sets — the rows the
26
+ # engine merges. A switched-off row counts as no row.
27
+ scope :in_effect, lambda {
28
+ active.joins(resource: :permission_set)
29
+ .merge(PermissionForge::Resource.active).merge(PermissionForge::PermissionSet.active)
30
+ }
31
+
32
+ # A duplicate (resource_id, name) row would let a second field entry with
33
+ # mask_access: false silently unmask the field via FieldPermissionMerger's
34
+ # AND logic. Enforced by a DB unique index (idx_pf_resource_fields_unique)
35
+ # as well; the validation surfaces a 422 instead of a RecordNotUnique 500.
36
+ # No case_sensitive option: the check then compares the way the column's
37
+ # collation does, and so the way the index does. On MySQL's default
38
+ # (case-insensitive) collation "Email" collides with "email" in the index;
39
+ # a forced case-sensitive check would let it through to a RecordNotUnique.
40
+ # Checked for a new row and on a rename only: a row whose name was
41
+ # blank before 0.14 stays editable in its other attributes.
42
+ validates :name, presence: { message: ->(*) { I18n.t('permission_forge.validations.name_blank') } },
43
+ if: -> { new_record? || will_save_change_to_name? }
44
+ validates :name, uniqueness: { scope: :resource_id }
45
+
46
+ validates_dependency :edit_access, requires: :read_access
47
+ validates_dependency :mask_access, requires: :read_access
48
+
49
+ validates :mask_strategy, presence: {
50
+ message: I18n.t('permission_forge.validations.resource_field.mask_strategy_required',
51
+ default: 'must be present when mask access is enabled')
52
+ },
53
+ if: :mask_access?
54
+ validates :mask_strategy, inclusion: {
55
+ in: ->(_) { PermissionForge::Services::MaskValue.strategies.keys },
56
+ message: I18n.t('permission_forge.validations.resource_field.invalid_mask_strategy',
57
+ default: 'is not a valid mask strategy')
58
+ },
59
+ if: :mask_access?
60
+
61
+ apply_tenant_scoping! # from the configuration, as the class loads (HasTenantScoping)
62
+
63
+ private
64
+
65
+ def mask_access?
66
+ mask_access == true
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,30 @@
1
+ <!DOCTYPE html>
2
+ <html lang="<%= I18n.locale %>" class="pf-root" data-theme="<%= pf_theme %>">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <meta name="robots" content="noindex, nofollow">
7
+ <title><%= [content_for(:pf_title).presence, t('permission_forge.layouts.application.title')].compact.join(' · ') %></title>
8
+ <%= csrf_meta_tags %>
9
+ <%= csp_meta_tag if respond_to?(:csp_meta_tag) %>
10
+ <link rel="icon" href="<%= pf_asset_url('favicon.ico') %>" sizes="any">
11
+ <link rel="icon" href="<%= pf_asset_url('favicon.svg') %>" type="image/svg+xml">
12
+ <link rel="apple-touch-icon" href="<%= pf_asset_url('apple-touch-icon.png') %>">
13
+ <link rel="stylesheet" href="<%= pf_asset_url('pf_admin.css') %>"<%= pf_nonce_attribute(:style) %>>
14
+ <script src="<%= pf_asset_url('pf_admin.js') %>" defer<%= pf_nonce_attribute(:script) %>></script>
15
+ <%= render 'permission_forge/shared/head_extra' %>
16
+ </head>
17
+ <body class="pf-body">
18
+ <a class="pf-skip" href="#pf-main"><%= t('permission_forge.layouts.application.skip_to_content') %></a>
19
+ <div class="pf-app">
20
+ <%= render 'permission_forge/shared/topbar' %>
21
+ <%= render 'permission_forge/shared/sidebar' %>
22
+ <main id="pf-main" class="pf-main" tabindex="-1">
23
+ <div class="pf-main__inner">
24
+ <%= yield %>
25
+ </div>
26
+ </main>
27
+ </div>
28
+ <%= render 'permission_forge/shared/toasts' %>
29
+ </body>
30
+ </html>
@@ -0,0 +1,2 @@
1
+ <%# Bare content for pages loaded into a drawer/dialog by pf_admin.js. %>
2
+ <%= yield %>
@@ -0,0 +1,26 @@
1
+ <%# Person picker rows. Locals: page (PeopleDirectory::Page), query, field, type ('radio'|'checkbox'),
2
+ selected (ids), invalid (error text). %>
3
+ <% selected = Array(local_assigns[:selected]).map(&:to_s) %>
4
+ <% invalid = local_assigns[:invalid] %>
5
+ <div class="pf-listbox" <%= pf_attrs_when(invalid, aria: { invalid: true }) %>>
6
+ <div class="pf-listbox__scroll" role="group" aria-label="<%= t('.label') %>">
7
+ <% if page.people.empty? %>
8
+ <p class="pf-option pf-text-3"><%= query.present? ? t('.no_match', query: query) : t('.nobody') %></p>
9
+ <% end %>
10
+ <% page.people.each do |person| %>
11
+ <% label = pf_person(person) %>
12
+ <label class="pf-option">
13
+ <input type="<%= type %>" name="<%= type == 'checkbox' ? "#{field}[]" : field %>" value="<%= person.id %>" <%= 'checked' if selected.include?(person.id.to_s) %>>
14
+ <%= pf_avatar(person, size: 'sm') %>
15
+ <span class="pf-option__main">
16
+ <span class="pf-fw-600 pf-ellipsis"><%= label.name %></span>
17
+ <% if label.secondary %><span class="pf-fs-12 pf-text-3 pf-ellipsis"><%= label.secondary %></span><% end %>
18
+ </span>
19
+ </label>
20
+ <% end %>
21
+ </div>
22
+ <% if page.total > page.people.size %>
23
+ <div class="pf-listbox__footer"><%= t('.capped', shown: page.people.size, total: page.total) %></div>
24
+ <% end %>
25
+ </div>
26
+ <% if invalid %><p class="pf-error" role="alert"><%= invalid %></p><% end %>
@@ -0,0 +1,24 @@
1
+ <% person = pf_person_name(@assignment.user) %>
2
+ <% set = @assignment.permission_set %>
3
+ <% content_for :pf_title, t('.title', set: set.name, name: person) %>
4
+ <%= pf_dialog_frame({ title: t('.title', set: set.name, name: person), danger: true,
5
+ breadcrumb: pf_breadcrumb([t('permission_forge.nav.people'), permission_forge.people_path], [person, permission_forge.person_path(@assignment.user_id)], t('.crumb')) }) do %>
6
+ <p class="pf-text-2">
7
+ <% first = pf_person(@assignment.user).first_name %>
8
+ <% if set.is_muting %>
9
+ <%= t('.body_muting', name: first, set: set.name) %>
10
+ <% elsif @lost.any? %>
11
+ <%= t('.body', name: first, resources: @lost.to_sentence) %>
12
+ <% else %>
13
+ <%= t('.body_nothing', name: first) %>
14
+ <% end %>
15
+ </p>
16
+ <%= form_with url: permission_forge.assignment_path(@assignment), method: :delete, class: 'pf-confirm-form' do %>
17
+ <%= render 'permission_forge/shared/reason_field' %>
18
+ <div class="pf-actions pf-row--right">
19
+ <input type="hidden" name="return_to" value="<%= params[:return_to] %>">
20
+ <%= link_to t('permission_forge.ui.cancel'), pf_back_or(permission_forge.person_path(@assignment.user_id)), class: 'pf-btn', data: { pf_close: true } %>
21
+ <button type="submit" class="pf-btn pf-btn--danger-solid"><%= t('permission_forge.ui.remove') %></button>
22
+ </div>
23
+ <% end %>
24
+ <% end %>
@@ -0,0 +1,99 @@
1
+ <% content_for :pf_title, t('.title') %>
2
+ <% errors = @errors || {} %>
3
+ <% title = if @person then t('.title_person', name: pf_person_name(@person))
4
+ elsif @preset_set then t('.title_set', set: @preset_set.name)
5
+ else t('.title') end %>
6
+ <% close_path = pf_back_or(@person ? permission_forge.person_path(@person) : permission_forge.people_path) %>
7
+ <%= form_with url: permission_forge.assignments_path, method: :post, id: 'pf-assign-form' do %>
8
+ <input type="hidden" name="return_to" value="<%= params[:return_to] %>">
9
+ <% footer = capture do %>
10
+ <span class="pf-sheet__footer-note">
11
+ <% if errors.any? %>
12
+ <% errors.each_value do |msg| %><span class="pf-error" role="alert"><%= msg %></span> <% end %>
13
+ <% elsif @person && @chosen_sets.any? %>
14
+ <%= t('.summary', name: pf_person_name(@person), sets: @chosen_sets.map(&:name).to_sentence,
15
+ until: (@expiry == 'never' ? t('.forever') : t('.until', date: pf_expiry_preset_date(@expiry) || @custom_date))) %>
16
+ <% else %>
17
+ <%= t('.applies_now') %>
18
+ <% end %>
19
+ </span>
20
+ <%= link_to t('permission_forge.ui.cancel'), close_path, class: 'pf-btn', data: { pf_close: true } %>
21
+ <button type="submit" class="pf-btn pf-btn--primary">
22
+ <%= @chosen_sets.size > 1 ? t('.assign_many', count: @chosen_sets.size) : t('.assign') %>
23
+ </button>
24
+ <% end %>
25
+
26
+ <%= pf_sheet_frame({ title: title, close_path: close_path, footer: footer }) do %>
27
+ <%# 1. Person %>
28
+ <section class="pf-stack" aria-labelledby="pf-assign-person">
29
+ <%= pf_section_heading("1. #{t('.person')}", id: 'pf-assign-person') %>
30
+ <% if @person %>
31
+ <input type="hidden" name="user_id" value="<%= @person.id %>">
32
+ <div class="pf-picked">
33
+ <%= pf_person_cell(@person) %>
34
+ <button type="submit" class="pf-btn pf-btn--xs pf-ml-auto" formmethod="get" formaction="<%= permission_forge.new_assignment_path %>"
35
+ name="change_person" value="1"><%= t('.change') %></button>
36
+ </div>
37
+ <% else %>
38
+ <div class="pf-row pf-row--nowrap">
39
+ <label class="pf-sr-only" for="pf-assign-q"><%= t('permission_forge.ui.search_people') %></label>
40
+ <input id="pf-assign-q" class="pf-input" type="search" name="q" value="<%= @query %>" autocomplete="off" autofocus
41
+ placeholder="<%= t('permission_forge.ui.search_people') %>"
42
+ data-pf-live="pf-assign-people" data-pf-live-url="<%= permission_forge.new_assignment_path %>">
43
+ <button type="submit" class="pf-btn pf-btn--sm" formmethod="get" formaction="<%= permission_forge.new_assignment_path %>" name="search" value="1"><%= t('permission_forge.ui.search') %></button>
44
+ </div>
45
+ <div id="pf-assign-people">
46
+ <%= render 'person_options', page: @people, query: @query, field: 'user_id', type: 'radio', invalid: errors[:person] %>
47
+ </div>
48
+ <% end %>
49
+ </section>
50
+
51
+ <%# 2. Permission Sets %>
52
+ <section class="pf-stack" aria-labelledby="pf-assign-sets">
53
+ <%= pf_section_heading("2. #{t('permission_forge.nav.permission_sets')}", id: 'pf-assign-sets') %>
54
+ <div class="pf-row pf-row--nowrap">
55
+ <label class="pf-sr-only" for="pf-assign-set-q"><%= t('.filter_sets') %></label>
56
+ <input id="pf-assign-set-q" class="pf-input pf-input--sm" type="search" name="set_q" value="<%= @set_query %>"
57
+ placeholder="<%= t('.filter_sets') %>" data-pf-live="pf-assign-set-list" data-pf-live-url="<%= permission_forge.new_assignment_path %>">
58
+ </div>
59
+ <div id="pf-assign-set-list" class="pf-listbox">
60
+ <div class="pf-listbox__scroll" role="group" aria-label="<%= t('permission_forge.nav.permission_sets') %>">
61
+ <% if @sets.empty? %>
62
+ <p class="pf-option pf-text-3"><%= t('.no_sets') %></p>
63
+ <% end %>
64
+ <% @sets.each do |set| %>
65
+ <% has = @existing_ids.include?(set.id) %>
66
+ <label class="pf-option">
67
+ <input type="checkbox" name="set_ids[]" value="<%= set.id %>" <%= 'checked' if @selected_ids.include?(set.id) && !has %> <%= 'disabled' if has %>>
68
+ <span class="pf-option__main">
69
+ <span class="pf-row pf-row--tight">
70
+ <span class="pf-fw-600"><%= set.name %></span>
71
+ <% if has %><%= pf_badge(t('.already')) %><% end %>
72
+ <% if set.is_muting %><%= pf_badge("⊘ #{t('permission_forge.ui.badges.muting')}", variant: :muting) %><% end %>
73
+ <% unless set.is_active %><%= pf_off_badge %><% end %>
74
+ </span>
75
+ <% if set.description.present? %><span class="pf-fs-12 pf-text-3"><%= set.description %></span><% end %>
76
+ </span>
77
+ <span class="pf-option__aside"><%= pf_people_text(@people_counts.fetch(set.id, 0)) %></span>
78
+ </label>
79
+ <% end %>
80
+ </div>
81
+ </div>
82
+ <% muting = @chosen_sets.select(&:is_muting) %>
83
+ <% if muting.any? %>
84
+ <div class="pf-banner pf-banner--muting">
85
+ <span class="pf-banner__glyph" aria-hidden="true">⊘</span>
86
+ <div class="pf-banner__body"><span><%= t('.muting_notice', sets: muting.map(&:name).to_sentence, name: @person ? pf_person(@person).first_name : t('.this_person')) %></span></div>
87
+ </div>
88
+ <% end %>
89
+ </section>
90
+
91
+ <%# 3. Expiry %>
92
+ <section class="pf-stack" aria-labelledby="pf-assign-expiry">
93
+ <%= pf_section_heading("3. #{t('.expires')}", id: 'pf-assign-expiry') %>
94
+ <%= render 'permission_forge/shared/expiry_picker', selected: @expiry, custom_date: @custom_date, error: errors[:expiry] %>
95
+ </section>
96
+
97
+ <%= render 'permission_forge/shared/reason_field', value: params[:reason] %>
98
+ <% end %>
99
+ <% end %>
@@ -0,0 +1,161 @@
1
+ <% content_for :pf_title, t('.title') %>
2
+ <% filters = @log.filters %>
3
+ <% value = ->(key) { pf_audit_filter_value(filters, key) } %>
4
+ <% invalid = ->(key) { @errors&.key?(key.to_sym) ? tag.attributes('aria-invalid': 'true') : '' } %>
5
+ <div class="pf-page-header">
6
+ <div class="pf-page-header__text">
7
+ <h1 class="pf-h1"><%= t('.title') %></h1>
8
+ <p class="pf-subtitle"><%= t('.subtitle') %></p>
9
+ </div>
10
+ <div class="pf-actions">
11
+ <%= link_to t('.holdings'), permission_forge.holdings_audit_events_path(format: :csv), class: 'pf-btn', title: t('.holdings_title') %>
12
+ </div>
13
+ </div>
14
+
15
+ <% if (problem = PermissionForge::Admin::AuditLog.recording_problem) %>
16
+ <%= render 'permission_forge/shared/audit_off_note', problem: problem %>
17
+ <% end %>
18
+
19
+ <% if @errors %>
20
+ <div class="pf-banner pf-banner--block" role="alert" id="pf-audit-errors">
21
+ <span class="pf-banner__glyph" aria-hidden="true">⊘</span>
22
+ <div class="pf-banner__body">
23
+ <span class="pf-banner__title"><%= t('.invalid.title') %></span>
24
+ <ul class="pf-audit-errors">
25
+ <% @errors.each do |name, messages| %>
26
+ <li><%= pf_audit_filter_error(name, messages, value.(name.to_s)) %></li>
27
+ <% end %>
28
+ </ul>
29
+ </div>
30
+ </div>
31
+ <% end %>
32
+
33
+ <%= form_with url: permission_forge.audit_events_path, method: :get, class: 'pf-card pf-audit-filters', role: 'search',
34
+ 'aria-label': t('.filters.label') do %>
35
+ <div class="pf-card__body pf-audit-filters__grid">
36
+ <p class="pf-hint pf-audit-filters__zone"><%= t('.filters.time_zone', zone: PermissionForge::Admin::AuditLogDays.zone_name) %></p>
37
+ <div class="pf-field">
38
+ <label class="pf-label" for="pf-audit-from"><%= t('.filters.from') %></label>
39
+ <input id="pf-audit-from" class="pf-input pf-input--sm" type="date" name="from" value="<%= value.('from') %>" <%= invalid.('from') %>>
40
+ </div>
41
+ <div class="pf-field">
42
+ <label class="pf-label" for="pf-audit-to"><%= t('.filters.to') %></label>
43
+ <input id="pf-audit-to" class="pf-input pf-input--sm" type="date" name="to" value="<%= value.('to') %>" <%= invalid.('to') %>>
44
+ </div>
45
+ <div class="pf-field">
46
+ <label class="pf-label" for="pf-audit-event"><%= t('.filters.event') %></label>
47
+ <select id="pf-audit-event" class="pf-select pf-select--sm" name="event" <%= invalid.('event') %>>
48
+ <option value=""><%= t('.filters.any_event') %></option>
49
+ <% PermissionForge::Admin::AuditLog.event_names_by_category.each do |category, names| %>
50
+ <optgroup label="<%= pf_audit_category_label(category) %>">
51
+ <% names.each do |name| %>
52
+ <option value="<%= name %>" <%= 'selected' if value.('event') == name %>><%= name %></option>
53
+ <% end %>
54
+ </optgroup>
55
+ <% end %>
56
+ <% if value.('event').present? && PermissionForge::Admin::AuditLog.event_names_by_category.values.flatten.exclude?(value.('event')) %>
57
+ <option value="<%= value.('event') %>" selected><%= value.('event') %></option>
58
+ <% end %>
59
+ </select>
60
+ </div>
61
+ <div class="pf-field">
62
+ <label class="pf-label" for="pf-audit-category"><%= t('.filters.category') %></label>
63
+ <select id="pf-audit-category" class="pf-select pf-select--sm" name="category" <%= invalid.('category') %>>
64
+ <option value=""><%= t('.filters.any_category') %></option>
65
+ <% PermissionForge::AuditEvent::CATEGORIES.each do |category| %>
66
+ <option value="<%= category %>" <%= 'selected' if value.('category') == category %>><%= pf_audit_category_label(category) %></option>
67
+ <% end %>
68
+ </select>
69
+ </div>
70
+ <div class="pf-field">
71
+ <label class="pf-label" for="pf-audit-actor-type"><%= t('.filters.actor_type') %></label>
72
+ <select id="pf-audit-actor-type" class="pf-select pf-select--sm" name="actor_type" <%= invalid.('actor_type') %>>
73
+ <option value=""><%= t('.filters.any_actor_type') %></option>
74
+ <% PermissionForge::AuditEvent::ACTOR_TYPES.each do |type| %>
75
+ <option value="<%= type %>" <%= 'selected' if value.('actor_type') == type %>><%= pf_audit_actor_type_label(type) %></option>
76
+ <% end %>
77
+ </select>
78
+ </div>
79
+ <div class="pf-field">
80
+ <label class="pf-label" for="pf-audit-actor"><%= t('.filters.actor_id') %></label>
81
+ <input id="pf-audit-actor" class="pf-input pf-input--sm" type="text" name="actor_id" value="<%= value.('actor_id') %>"
82
+ list="pf-audit-people" autocomplete="off" placeholder="<%= t('.filters.id_placeholder') %>" <%= invalid.('actor_id') %>>
83
+ </div>
84
+ <div class="pf-field">
85
+ <label class="pf-label" for="pf-audit-person"><%= t('.filters.subject_user_id') %></label>
86
+ <input id="pf-audit-person" class="pf-input pf-input--sm" type="text" name="subject_user_id" value="<%= value.('subject_user_id') %>"
87
+ list="pf-audit-people" autocomplete="off" placeholder="<%= t('.filters.id_placeholder') %>" <%= invalid.('subject_user_id') %>>
88
+ </div>
89
+ <div class="pf-field">
90
+ <label class="pf-label" for="pf-audit-set"><%= t('.filters.permission_set') %></label>
91
+ <% sets = PermissionForge::PermissionSet.order(:name).pluck(:id, :name) %>
92
+ <select id="pf-audit-set" class="pf-select pf-select--sm" name="permission_set" <%= invalid.('permission_set') %>>
93
+ <option value=""><%= t('.filters.any_set') %></option>
94
+ <% sets.each do |id, name| %>
95
+ <option value="<%= id %>" <%= 'selected' if value.('permission_set') == id.to_s %>><%= name %></option>
96
+ <% end %>
97
+ <% if value.('permission_set').present? && sets.none? { |id, _| id.to_s == value.('permission_set') } %>
98
+ <option value="<%= value.('permission_set') %>" selected><%= t('.filters.deleted_set', id: value.('permission_set')) %></option>
99
+ <% end %>
100
+ </select>
101
+ </div>
102
+ <div class="pf-field">
103
+ <label class="pf-label" for="pf-audit-correlation"><%= t('.filters.correlation_id') %></label>
104
+ <input id="pf-audit-correlation" class="pf-input pf-input--sm" type="text" name="correlation_id" value="<%= value.('correlation_id') %>"
105
+ autocomplete="off" <%= invalid.('correlation_id') %>>
106
+ </div>
107
+ <% record_filter = %w[record_type record_id record_uuid].any? { |key| filters.key?(key) } %>
108
+ <% %w[record_type record_id record_uuid].each do |key| %>
109
+ <% if record_filter %>
110
+ <div class="pf-field">
111
+ <label class="pf-label" for="pf-audit-<%= key.dasherize %>"><%= t(".filters.#{key}") %></label>
112
+ <input id="pf-audit-<%= key.dasherize %>" class="pf-input pf-input--sm" type="text" name="<%= key %>" value="<%= value.(key) %>"
113
+ autocomplete="off" <%= invalid.(key) %>>
114
+ </div>
115
+ <% end %>
116
+ <% end %>
117
+ <% unless @errors %>
118
+ <datalist id="pf-audit-people">
119
+ <% PermissionForge::Admin::AuditLog.known_people.each do |id, label| %>
120
+ <option value="<%= id %>"><%= label.presence || t('permission_forge.audit_log.person_id', id: id) %></option>
121
+ <% end %>
122
+ </datalist>
123
+ <% end %>
124
+ <label class="pf-checkbox pf-audit-filters__views">
125
+ <input type="checkbox" name="views" value="1" <%= 'checked' if params[:views].to_s == '1' %>>
126
+ <span><%= t('.filters.views') %></span>
127
+ </label>
128
+ </div>
129
+ <div class="pf-card__footer pf-card__footer--muted">
130
+ <button type="submit" class="pf-btn pf-btn--primary pf-btn--sm"><%= t('.filters.apply') %></button>
131
+ <% if @log.link_params.any? %>
132
+ <%= link_to t('.filters.clear'), permission_forge.audit_events_path, class: 'pf-btn pf-btn--sm' %>
133
+ <% end %>
134
+ <% unless @errors %>
135
+ <span class="pf-ml-auto pf-row pf-row--tight">
136
+ <span class="pf-fs-13 pf-text-2"><%= t('.export') %></span>
137
+ <%= link_to 'CSV', permission_forge.audit_events_path(filters.merge(format: :csv)), class: 'pf-btn pf-btn--sm', title: t('.export_title', kind: 'CSV') %>
138
+ <%= link_to 'JSON', permission_forge.audit_events_path(filters.merge(format: :json)), class: 'pf-btn pf-btn--sm', title: t('.export_title', kind: 'JSON') %>
139
+ </span>
140
+ <% end %>
141
+ </div>
142
+ <% end %>
143
+
144
+ <% if @page %>
145
+ <section class="pf-card" aria-labelledby="pf-audit-results">
146
+ <div class="pf-card__header">
147
+ <h2 class="pf-h2" id="pf-audit-results"><%= t('.results', count: @page.total) %></h2>
148
+ <span class="pf-card__desc"><%= t('.newest_first') %></span>
149
+ </div>
150
+ <% unless @log.include_views? %>
151
+ <p class="pf-card__row pf-fs-13 pf-text-2 pf-audit-views-note">
152
+ <%= t('.views_hidden') %>
153
+ <%= link_to t('.views_show'), permission_forge.audit_events_path(@log.link_params.merge('views' => '1')) %>
154
+ </p>
155
+ <% end %>
156
+ <%= render 'permission_forge/shared/audit_events', events: @page.events, lookups: @lookups,
157
+ empty: (filters.any? ? t('.empty_filtered') : t('permission_forge.audit_log.empty')) %>
158
+ <%= render 'permission_forge/shared/audit_pagination', page: @page,
159
+ path: ->(number) { permission_forge.audit_events_path(@log.link_params.merge(page: number)) } %>
160
+ </section>
161
+ <% end %>
@@ -0,0 +1,146 @@
1
+ <% d = @detail %>
2
+ <% event = d.event %>
3
+ <% lookups = d.lookups %>
4
+ <% content_for :pf_title, t('.title', event: event.event) %>
5
+ <%= pf_breadcrumb([t('permission_forge.audit_events.index.title'), permission_forge.audit_events_path], event.event) %>
6
+
7
+ <div class="pf-page-header">
8
+ <div class="pf-page-header__text">
9
+ <div class="pf-row pf-row--tight">
10
+ <%= pf_audit_category_badge(event.category) %>
11
+ <span class="pf-mono-key"><%= event.event %></span>
12
+ </div>
13
+ <h1 class="pf-h1"><%= pf_audit_sentence(event, lookups) %></h1>
14
+ <p class="pf-subtitle"><%= pf_datetime(event.occurred_at) %> · <time datetime="<%= event.occurred_at.utc.iso8601(6) %>"><%= event.occurred_at.utc.iso8601 %></time></p>
15
+ </div>
16
+ </div>
17
+
18
+ <div class="pf-grid-2">
19
+ <section class="pf-card" aria-labelledby="pf-event-who">
20
+ <div class="pf-card__header"><h2 class="pf-h2" id="pf-event-who"><%= t('.who') %></h2></div>
21
+ <dl class="pf-card__body pf-kv">
22
+ <dt><%= t('.actor') %></dt>
23
+ <dd>
24
+ <span class="pf-fw-600"><%= pf_audit_actor_name(event) %></span>
25
+ <span class="pf-text-3">· <%= pf_audit_actor_type_label(event.actor_type) %><% if d.actor_id? %> · <%= t('.id', id: event.actor_id) %><% end %></span>
26
+ <div class="pf-row pf-row--tight pf-fs-12">
27
+ <%= link_to t('.all_by_actor'), pf_audit_log_path(actor_type: event.actor_type, actor_id: event.actor_id) %>
28
+ <% if d.actor_user %>· <%= link_to t('.open_person'), permission_forge.person_path(d.actor_user) %><% end %>
29
+ </div>
30
+ </dd>
31
+ <dt><%= t('.source') %></dt>
32
+ <dd><%= pf_audit_source_label(event.source) %> <span class="pf-mono-key"><%= event.source %></span></dd>
33
+ <dt><%= t('.reason') %></dt>
34
+ <dd><%= event.reason.presence || t('.no_reason') %></dd>
35
+ <dt><%= t('.request_id') %></dt>
36
+ <dd class="pf-audit-code"><%= event.request_id.presence || t('permission_forge.audit_log.no_value') %></dd>
37
+ <dt><%= t('.ip') %></dt>
38
+ <dd class="pf-audit-code"><%= event.ip.presence || t('permission_forge.audit_log.no_value') %></dd>
39
+ </dl>
40
+ </section>
41
+
42
+ <section class="pf-card" aria-labelledby="pf-event-what">
43
+ <div class="pf-card__header"><h2 class="pf-h2" id="pf-event-what"><%= t('.what') %></h2></div>
44
+ <dl class="pf-card__body pf-kv">
45
+ <dt><%= t('.record') %></dt>
46
+ <dd>
47
+ <% if d.record? %>
48
+ <span class="pf-fw-600"><%= pf_audit_record_name(event) %></span>
49
+ <div class="pf-fs-12 pf-text-3"><span class="pf-audit-code"><%= event.record_type %></span><% if d.record_id? %> · <%= t('.id', id: event.record_id) %><% end %></div>
50
+ <% if d.record_uuid? %><div class="pf-fs-12 pf-audit-code pf-text-3"><%= event.record_uuid %></div><% end %>
51
+ <% if d.record_id? %>
52
+ <div class="pf-fs-12"><%= link_to t('.all_for_record'), pf_audit_log_path(record_type: event.record_type, record_id: event.record_id) %></div>
53
+ <% end %>
54
+ <% else %>
55
+ <%= t('permission_forge.audit_log.no_value') %>
56
+ <% end %>
57
+ </dd>
58
+ <dt><%= t('.permission_set') %></dt>
59
+ <dd>
60
+ <% if d.permission_set? %>
61
+ <% if d.permission_set %><%= link_to pf_audit_set_name(event, lookups), permission_forge.permission_set_path(d.permission_set) %><% else %><%= pf_audit_set_name(event, lookups) %><% end %>
62
+ <div class="pf-fs-12"><%= link_to t('.all_for_set'), pf_audit_log_path(permission_set: d.permission_set_key) %></div>
63
+ <% else %>
64
+ <%= t('permission_forge.audit_log.no_value') %>
65
+ <% end %>
66
+ </dd>
67
+ <dt><%= t('.person') %></dt>
68
+ <dd>
69
+ <% if d.subject_user? %>
70
+ <% if d.person %><%= link_to pf_audit_person_name(event, lookups), permission_forge.person_path(d.person) %><% else %><%= pf_audit_person_name(event, lookups) %><% end %>
71
+ <span class="pf-text-3">· <%= t('.id', id: event.subject_user_id) %></span>
72
+ <div class="pf-fs-12"><%= link_to t('.all_for_person'), pf_audit_log_path(subject_user_id: event.subject_user_id) %></div>
73
+ <% else %>
74
+ <%= t('permission_forge.audit_log.no_value') %>
75
+ <% end %>
76
+ </dd>
77
+ <dt><%= t('.correlation') %></dt>
78
+ <dd>
79
+ <% if d.correlation? %>
80
+ <span class="pf-audit-code pf-fs-12"><%= event.correlation_id %></span>
81
+ <div class="pf-fs-12"><%= link_to t('.all_in_action'), pf_audit_log_path(correlation_id: event.correlation_id) %></div>
82
+ <% else %>
83
+ <%= t('permission_forge.audit_log.no_value') %>
84
+ <% end %>
85
+ </dd>
86
+ <dt><%= t('.uuid') %></dt>
87
+ <dd class="pf-audit-code pf-fs-12"><%= event.uuid %></dd>
88
+ </dl>
89
+ </section>
90
+ </div>
91
+
92
+ <% if d.changes? %>
93
+ <section class="pf-card pf-scroll-x" aria-labelledby="pf-event-changes">
94
+ <div class="pf-card__header">
95
+ <h2 class="pf-h2" id="pf-event-changes"><%= t('.changes') %></h2>
96
+ <span class="pf-card__desc"><%= t('.changes_desc', count: d.changes_count) %></span>
97
+ </div>
98
+ <table class="pf-audit-table">
99
+ <thead><tr><th scope="col"><%= t('.attribute') %></th><th scope="col"><%= t('.before') %></th><th scope="col"><%= t('.after') %></th></tr></thead>
100
+ <tbody>
101
+ <% d.change_rows.each do |attribute, from, to| %>
102
+ <tr>
103
+ <th scope="row" class="pf-audit-code"><%= attribute %></th>
104
+ <td class="pf-audit-table__before"><span class="pf-sr-only"><%= t('.before') %>: </span><%= pf_audit_value(from) %></td>
105
+ <td class="pf-audit-table__after"><span aria-hidden="true">→ </span><span class="pf-sr-only"><%= t('.after') %>: </span><%= pf_audit_value(to) %></td>
106
+ </tr>
107
+ <% end %>
108
+ </tbody>
109
+ </table>
110
+ </section>
111
+ <% end %>
112
+
113
+ <% if d.snapshot? %>
114
+ <section class="pf-card pf-scroll-x" aria-labelledby="pf-event-snapshot">
115
+ <div class="pf-card__header">
116
+ <h2 class="pf-h2" id="pf-event-snapshot"><%= t('.snapshot') %></h2>
117
+ <span class="pf-card__desc"><%= d.snapshot_of_removed? ? t('.snapshot_destroyed') : t('.snapshot_desc') %></span>
118
+ </div>
119
+ <table class="pf-audit-table">
120
+ <thead><tr><th scope="col"><%= t('.attribute') %></th><th scope="col"><%= t('.value') %></th></tr></thead>
121
+ <tbody>
122
+ <% d.snapshot_rows.each do |attribute, attribute_value| %>
123
+ <tr><th scope="row" class="pf-audit-code"><%= attribute %></th><td><%= pf_audit_value(attribute_value) %></td></tr>
124
+ <% end %>
125
+ </tbody>
126
+ </table>
127
+ </section>
128
+ <% end %>
129
+
130
+ <% if d.other? %>
131
+ <section class="pf-card pf-scroll-x" aria-labelledby="pf-event-data">
132
+ <div class="pf-card__header"><h2 class="pf-h2" id="pf-event-data"><%= t('.data') %></h2></div>
133
+ <table class="pf-audit-table">
134
+ <thead><tr><th scope="col"><%= t('.key') %></th><th scope="col"><%= t('.value') %></th></tr></thead>
135
+ <tbody>
136
+ <% d.other_rows.each do |key, data_value| %>
137
+ <tr><th scope="row" class="pf-audit-code"><%= key %></th><td class="pf-audit-code"><%= pf_audit_value(data_value) %></td></tr>
138
+ <% end %>
139
+ </tbody>
140
+ </table>
141
+ </section>
142
+ <% end %>
143
+
144
+ <% if d.no_data? %>
145
+ <p class="pf-fs-13 pf-text-3"><%= t('.no_data') %></p>
146
+ <% end %>
@@ -0,0 +1,21 @@
1
+ <% set = @permission_set %>
2
+ <% back = permission_forge.permission_set_path(set, tab: 'assignments') %>
3
+ <% content_for :pf_title, t('.title', count: @people.size, set: set.name) %>
4
+ <%= pf_dialog_frame({ title: t('.title', count: @people.size, set: set.name), danger: true,
5
+ breadcrumb: pf_breadcrumb([t('permission_forge.nav.permission_sets'), permission_forge.permission_sets_path], [set.name, back], t('.crumb')) }) do %>
6
+ <% if @people.empty? %>
7
+ <p class="pf-text-2"><%= t('.none') %></p>
8
+ <div class="pf-actions pf-row--right"><%= link_to t('permission_forge.ui.close'), back, class: 'pf-btn', data: { pf_close: true } %></div>
9
+ <% else %>
10
+ <p class="pf-text-2"><%= t('.body') %></p>
11
+ <ul class="pf-confirm__list"><% @people.each do |u| %><li><%= pf_person_name(u) %></li><% end %></ul>
12
+ <%= form_with url: permission_forge.revoke_permission_set_bulk_assignment_path(set), method: :delete, class: 'pf-confirm-form' do %>
13
+ <%= render 'permission_forge/shared/reason_field' %>
14
+ <div class="pf-actions pf-row--right">
15
+ <% @people.each do |u| %><input type="hidden" name="user_ids[]" value="<%= u.id %>"><% end %>
16
+ <%= link_to t('permission_forge.ui.cancel'), back, class: 'pf-btn', data: { pf_close: true } %>
17
+ <button type="submit" class="pf-btn pf-btn--danger-solid"><%= t('.confirm', count: @people.size) %></button>
18
+ </div>
19
+ <% end %>
20
+ <% end %>
21
+ <% end %>