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,117 @@
1
+ <% set = @permission_set %>
2
+ <% resource = @resource %>
3
+ <% pf_audit_preload_last_changes([resource, *(@tab == 'rules' ? @rules : [])]) %>
4
+ <% muting = set.is_muting %>
5
+ <% locked = pf_locked_record?(set) || pf_locked_record?(resource) %>
6
+ <% label = pf_resource_label(resource) %>
7
+ <% plural = pf_resource_plural(resource.name) %>
8
+ <% base = "pf[resources][#{resource.id}]" %>
9
+ <% content_for :pf_title, "#{label} · #{set.name}" %>
10
+ <%= pf_breadcrumb([t('permission_forge.nav.permission_sets'), permission_forge.permission_sets_path],
11
+ [set.name, permission_forge.permission_set_path(set)], label) %>
12
+
13
+ <% if muting %>
14
+ <div class="pf-banner pf-banner--muting" role="note">
15
+ <span class="pf-banner__glyph" aria-hidden="true">⊘</span>
16
+ <div class="pf-banner__body"><span><b><%= t('permission_forge.ui.badges.muting') %>.</b> <%= t('.muting_note', set: set.name) %></span></div>
17
+ </div>
18
+ <% end %>
19
+ <%= render 'permission_forge/shared/mode_banners', record: (pf_locked_record?(resource) ? resource : set), muting: false,
20
+ duplicate_path: permission_forge.duplicate_permission_set_path(set) %>
21
+ <%= pf_help_banner('resource', term: t('.help_term'), text: t('.help_text', model: resource.name)) %>
22
+
23
+ <%= form_with url: permission_forge.review_permission_set_resource_path(set, resource.name), method: :post, id: 'pf-res-form',
24
+ data: { pf_track: 'pf-res-savebar', pf_review: true } do %>
25
+ <input type="hidden" name="return_to" value="<%= request.fullpath %>">
26
+ <%# The form's default button. Without it, Enter in a text input submits through the first
27
+ submit button on the page — a bulk "Set to" button, proposing that level for the ticked rows.
28
+ Out of sight but rendered, not `hidden`: Safari before 16.4 skips a display:none button
29
+ when it picks the default button (webkit.org/b/247545) and would press a bulk button. %>
30
+ <button type="submit" class="pf-sr-only" tabindex="-1" aria-hidden="true"><%= t('permission_forge.shared.savebar.review') %></button>
31
+ <% end %>
32
+
33
+ <div class="pf-page-header">
34
+ <div class="pf-page-header__text">
35
+ <div class="pf-row">
36
+ <h1 class="pf-h1"><%= label %></h1>
37
+ <code class="pf-mono-key"><%= resource.name %></code>
38
+ </div>
39
+ <label class="pf-sr-only" for="pf-res-desc"><%= t('.description') %></label>
40
+ <input id="pf-res-desc" class="pf-desc-input" type="text" name="<%= base %>[description]" value="<%= resource.description %>"
41
+ form="pf-res-form" placeholder="<%= t('.description_placeholder') %>" <%= 'readonly' if locked %>>
42
+ <span class="pf-impact"><span class="pf-dot" aria-hidden="true"></span><%= t('.impact', people: pf_people_text(@people_count)) %></span>
43
+ </div>
44
+ <label class="pf-switch">
45
+ <input type="hidden" name="<%= base %>[is_active]" value="0" form="pf-res-form">
46
+ <input type="checkbox" role="switch" name="<%= base %>[is_active]" value="1" form="pf-res-form" <%= 'checked' if resource.is_active %> <%= 'disabled' if locked %>>
47
+ <span class="pf-switch__track" aria-hidden="true"></span>
48
+ <span class="pf-switch__on"><%= t('permission_forge.shared.active') %></span>
49
+ <span class="pf-switch__off"><%= t('permission_forge.ui.badges.off') %></span>
50
+ </label>
51
+ </div>
52
+
53
+ <div class="pf-grid-2" data-pf-crud="r<%= resource.id %>">
54
+ <section class="pf-card" aria-labelledby="pf-res-crud">
55
+ <div class="pf-card__body pf-stack">
56
+ <h2 class="pf-h2" id="pf-res-crud"><%= t('.what_can_do') %></h2>
57
+ <div class="pf-crud-grid">
58
+ <% PermissionForge::Admin::EffectiveAccess::CRUD.each do |flag| %>
59
+ <div class="pf-crud-grid__col">
60
+ <span class="pf-fs-12 pf-text-3"><%= t("permission_forge.ui.flags_short.#{flag}") %></span>
61
+ <%= render 'permission_forge/shared/crud_cell', name: "#{base}[#{flag}]", flag: flag, checked: resource.public_send(flag),
62
+ muting: muting, locked: locked, size: :lg, form: 'pf-res-form',
63
+ requires: (%w[edit_access delete_access].include?(flag.to_s) ? 'read_access' : nil),
64
+ aria: "#{t("permission_forge.ui.flags_short.#{flag}")} #{plural}" %>
65
+ </div>
66
+ <% end %>
67
+ </div>
68
+ <span class="pf-hint"><%= muting ? t('.muting_hint') : t('.needs_read_hint') %></span>
69
+ </div>
70
+ </section>
71
+
72
+ <% unless muting %>
73
+ <section class="pf-card" aria-labelledby="pf-res-scope">
74
+ <div class="pf-card__body pf-stack">
75
+ <h2 class="pf-h2" id="pf-res-scope"><%= t('.record_scope') %></h2>
76
+ <% { 'view_all' => 'read_access', 'modify_all' => 'view_all edit_access' }.each do |flag, requires| %>
77
+ <label class="pf-scope-option">
78
+ <input type="hidden" name="<%= base %>[<%= flag %>]" value="0" form="pf-res-form">
79
+ <input type="checkbox" name="<%= base %>[<%= flag %>]" value="1" form="pf-res-form" <%= 'checked' if resource.public_send(flag) %>
80
+ <%= 'disabled' if locked %> data-pf-flag="<%= flag %>" data-pf-requires="<%= requires %>" <%= 'data-pf-locked' if locked %>>
81
+ <span class="pf-fs-13"><%= t(".#{flag}_html", resource: label) %>
82
+ <span class="pf-text-3"><%= t(".#{flag}_needs") %></span></span>
83
+ </label>
84
+ <% end %>
85
+ <% active_rules = @rules.count(&:is_active) %>
86
+ <% if resource.view_all && active_rules.positive? %>
87
+ <span class="pf-fs-13 pf-text-warn">⚠ <%= t('.rules_ignored', count: active_rules) %></span>
88
+ <% end %>
89
+ </div>
90
+ </section>
91
+ <% end %>
92
+ </div>
93
+
94
+ <% if @rows.sync_needed? && !locked %>
95
+ <%= render 'sync', set: set, resource: resource, muting: muting %>
96
+ <% end %>
97
+
98
+ <nav class="pf-tabs" role="tablist" aria-label="<%= t('.tabs_label') %>">
99
+ <%= pf_tab t('permission_forge.ui.fields'), permission_forge.permission_set_resource_path(set, resource.name), selected: @tab == 'fields', count: @rows.rows.size %>
100
+ <%= pf_tab t('permission_forge.ui.actions'), permission_forge.permission_set_resource_path(set, resource.name, tab: 'actions'), selected: @tab == 'actions', count: @action_rows.rows.size %>
101
+ <%= pf_tab t('permission_forge.ui.rules_tab'), permission_forge.permission_set_resource_path(set, resource.name, tab: 'rules'), selected: @tab == 'rules', count: @rules.size %>
102
+ </nav>
103
+
104
+ <div role="tabpanel">
105
+ <%= render "#{@tab}_tab", set: set, resource: resource, muting: muting, locked: locked, base: base %>
106
+ </div>
107
+
108
+ <% unless locked %>
109
+ <%= render 'permission_forge/shared/savebar', id: 'pf-res-savebar', form: 'pf-res-form', people: @people_count %>
110
+ <% end %>
111
+
112
+ <%= pf_tech_details(resource, extras: { model: resource.name }) do %>
113
+ <% unless locked %>
114
+ <div><%= pf_modal_link t('.remove', resource: label), permission_forge.delete_permission_set_resource_path(set, resource.name), class: 'pf-btn pf-btn--danger pf-btn--xs' %></div>
115
+ <% end %>
116
+ <% end %>
117
+ <%= pf_audit_footer(resource) %>
@@ -0,0 +1,15 @@
1
+ <% config = PermissionForge.configuration %>
2
+ <% tiles = [['enforce', 'auto_query_scoping', config.auto_query_scoping.to_s != 'off', config.auto_query_scoping],
3
+ ['detect', 'unscoped_query_detection', config.unscoped_query_detection.to_s != 'off', config.unscoped_query_detection],
4
+ ['database', 'database_row_level_security', config.database_row_level_security == true, config.database_row_level_security]] %>
5
+ <div class="pf-stack pf-stack--tight">
6
+ <p class="pf-fs-13 pf-text-2 pf-setting"><%= t('.intro') %></p>
7
+ <div class="pf-ladder">
8
+ <% tiles.each do |level, key, on, value| %>
9
+ <%= link_to permission_forge.settings_path(focus: key, anchor: "set-#{key}"), class: "pf-ladder__tile#{' pf-ladder__tile--on' if on}" do %>
10
+ <span class="pf-fw-600"><%= on ? '✓' : '–' %> <%= t(".#{level}") %> · <%= on ? t('.active') : t('.off') %></span>
11
+ <code class="pf-mono-key"><%= key %> = <%= value.inspect %></code>
12
+ <% end %>
13
+ <% end %>
14
+ </div>
15
+ </div>
@@ -0,0 +1,38 @@
1
+ <% key = row.key %>
2
+ <% name = t("permission_forge.settings.items.#{key}.name") %>
3
+ <% desc = t("permission_forge.settings.items.#{key}.desc") %>
4
+ <div class="pf-setting" id="set-<%= key %>" data-pf-setting-row="<%= "#{key} #{name} #{desc}".downcase %>" <%= pf_attrs_when(focused, data: { pf_focus: true }) %>>
5
+ <div class="pf-setting__about">
6
+ <div class="pf-row pf-row--tight">
7
+ <b><%= name %></b>
8
+ <%= pf_badge(t('.changed'), variant: :changed) if row.changed? %>
9
+ <%= pf_badge("⚠ #{t('.risk')}", variant: :warn) if row.warn? %>
10
+ <%= pf_badge("✓ #{t('.secure')}", variant: :secure) if row.ok? %>
11
+ </div>
12
+ <span class="pf-fs-13 pf-text-2"><%= desc %></span>
13
+ <code class="pf-mono-key"><%= key %></code>
14
+ <% if row.warn? %><span class="pf-fs-13 pf-text-warn"><%= t("permission_forge.settings.risks.#{key}") %></span><% end %>
15
+ <% if row.ok? %><span class="pf-fs-12 pf-text-grant"><%= t("permission_forge.settings.oks.#{key}") %></span><% end %>
16
+ </div>
17
+ <div class="pf-setting__value">
18
+ <%= pf_setting_value(row) %>
19
+ <span class="pf-fs-12 pf-text-3"><%= t('.default', value: pf_setting_default(row)) %></span>
20
+ </div>
21
+ <details class="pf-setting__how" <%= 'open' if focused %>>
22
+ <summary class="pf-btn pf-btn--sm"><%= t('.how_to_change') %></summary>
23
+ <div class="pf-setting__panel">
24
+ <% snippet = "# config/initializers/permission_forge.rb\nPermissionForge.configure do |config|\n config.#{key} = #{@catalog.snippet_value(row)}\nend" %>
25
+ <div class="pf-row pf-row--between">
26
+ <span class="pf-fs-12 pf-text-3"><%= row.warn? && @catalog.recommended_value(key) ? t('.snippet_recommended') : t('.snippet_current') %></span>
27
+ <button type="button" class="pf-btn pf-btn--xs" data-pf-copy="<%= snippet %>" data-pf-copied="<%= t('permission_forge.shared.tech_details.copied') %>" hidden data-pf-js-show><%= t('permission_forge.shared.tech_details.copy') %></button>
28
+ </div>
29
+ <pre class="pf-code"><%= snippet %></pre>
30
+ <ol class="pf-stack pf-stack--tight pf-fs-13">
31
+ <li>1. <%= t('.step_edit') %></li>
32
+ <li>2. <%= t('.step_deploy') %></li>
33
+ <li>3. <%= t('.step_reload') %></li>
34
+ </ol>
35
+ <%= link_to t('.docs', key: key), "#{PermissionForge.configuration.admin_docs_url}##{key}", class: 'pf-fs-12', target: '_blank', rel: 'noopener noreferrer' %>
36
+ </div>
37
+ </details>
38
+ </div>
@@ -0,0 +1,67 @@
1
+ <% content_for :pf_title, t('permission_forge.nav.settings') %>
2
+ <% risky = @catalog.risky_rows %>
3
+ <div class="pf-page-header">
4
+ <div class="pf-page-header__text">
5
+ <div class="pf-row">
6
+ <h1 class="pf-h1"><%= t('permission_forge.nav.settings') %></h1>
7
+ <code class="pf-mono-key">PermissionForge <%= PermissionForge::VERSION %></code>
8
+ <span class="pf-pill"><%= t('.read_only') %></span>
9
+ </div>
10
+ <p class="pf-subtitle"><%= t('.intro_html', file: content_tag(:code, 'config/initializers/permission_forge.rb')) %></p>
11
+ </div>
12
+ </div>
13
+
14
+ <section class="pf-card" aria-labelledby="pf-health">
15
+ <div class="pf-card__header"><h2 class="pf-h2" id="pf-health"><%= t('.health') %></h2></div>
16
+ <% risky.each do |row| %>
17
+ <div class="pf-card__row pf-card__row--top">
18
+ <span class="pf-text-warn" aria-hidden="true">⚠</span>
19
+ <span class="pf-stack pf-stack--tight pf-grow">
20
+ <b><%= t("permission_forge.settings.items.#{row.key}.name") %></b>
21
+ <span class="pf-fs-13 pf-text-2"><%= t("permission_forge.settings.risks.#{row.key}") %></span>
22
+ <code class="pf-mono-key"><%= row.key %> = <%= pf_setting_inline_value(row) %></code>
23
+ </span>
24
+ <%= link_to t('.how_to_fix'), permission_forge.settings_path(focus: row.key, anchor: "set-#{row.key}"), class: 'pf-btn pf-btn--sm' %>
25
+ </div>
26
+ <% end %>
27
+ <div class="pf-card__row">
28
+ <span class="pf-text-grant">✓ <%= t('.secure_count', count: @catalog.secure_count) %></span>
29
+ </div>
30
+ </section>
31
+
32
+ <%= form_with url: permission_forge.settings_path, method: :get, class: 'pf-row', role: 'search' do %>
33
+ <label class="pf-sr-only" for="pf-settings-q"><%= t('.search') %></label>
34
+ <input id="pf-settings-q" class="pf-input pf-input--auto pf-grow" type="search" name="q" value="<%= params[:q] %>"
35
+ placeholder="<%= t('.search') %>" data-pf-settings-filter>
36
+ <label class="pf-checkbox">
37
+ <input type="checkbox" name="changed" value="1" <%= 'checked' if @changed_only %> data-pf-autosubmit>
38
+ <span><%= t('.changed_only') %></span>
39
+ </label>
40
+ <button type="submit" class="pf-btn pf-btn--sm" data-pf-js-hide><%= t('permission_forge.ui.apply') %></button>
41
+ <% end %>
42
+
43
+ <div class="pf-card pf-card--visible">
44
+ <% if @sections.empty? %><p class="pf-empty"><%= t('.no_match') %></p><% end %>
45
+ <% @sections.each do |section, rows| %>
46
+ <% section_risky = rows.any?(&:warn?) %>
47
+ <% open = @focus.present? ? rows.any? { |r| r.key == @focus } : (@query.present? || @changed_only || section_risky) %>
48
+ <details class="pf-setting-section" data-pf-setting-section <%= 'open' if open %>>
49
+ <summary>
50
+ <span class="pf-disclosure" aria-hidden="true">▸</span>
51
+ <span class="pf-h2"><%= t("permission_forge.settings.sections.#{section}") %></span>
52
+ <% if section_risky %><span class="pf-text-warn" aria-label="<%= t('.has_risk') %>">⚠</span><% end %>
53
+ <span class="pf-fs-12 pf-text-3 pf-ml-auto"><%= t('.section_meta', settings: t('.settings_count', count: rows.size), changed: t('.changed_count', count: rows.count(&:changed?))) %></span>
54
+ </summary>
55
+ <% if section == 'admin_api' && !PermissionForge.configuration.api_enabled && !@query.present? %>
56
+ <p class="pf-setting pf-text-2"><%= t('.api_disabled_html', key: content_tag(:code, 'api_enabled = false')) %></p>
57
+ <% rows = rows.select { |r| r.key == 'api_enabled' } %>
58
+ <% end %>
59
+ <% if section == 'record_rules' %>
60
+ <%= render 'ladder' %>
61
+ <% end %>
62
+ <% rows.each do |row| %>
63
+ <%= render 'row', row: row, focused: row.key == @focus %>
64
+ <% end %>
65
+ </details>
66
+ <% end %>
67
+ </div>
@@ -0,0 +1,31 @@
1
+ <%# "Who can see this field?" — one row per Permission Set containing the resource.
2
+ Locals: rows ([{ resource:, set:, field:, people:, in_effect: }] from Admin::FieldAcrossSets), key, current_set (opt) %>
3
+ <% if rows.empty? %>
4
+ <p class="pf-fs-13 pf-text-3"><%= t('.none') %></p>
5
+ <% else %>
6
+ <div class="pf-listbox">
7
+ <% rows.each do |row| %>
8
+ <% set = row[:set] %>
9
+ <% field = row[:field] %>
10
+ <% level = field && PermissionForge::Admin::FieldLevel.level_of(field, muting: set.is_muting) %>
11
+ <%= link_to permission_forge.permission_set_resource_field_path(set, row[:resource].name, key),
12
+ class: 'pf-option', 'aria-current': (local_assigns[:current_set] == set ? 'true' : nil) do %>
13
+ <span class="pf-option__main">
14
+ <span class="pf-row pf-row--tight">
15
+ <% if set.is_muting %><span class="pf-text-restrict" aria-hidden="true">⊘</span><% end %>
16
+ <span class="pf-fw-600"><%= set.name %></span>
17
+ <%= pf_off_badge unless row[:in_effect] %>
18
+ </span>
19
+ </span>
20
+ <% if level.nil? %>
21
+ <%= pf_level_badge('none', label: t('permission_forge.ui.badges.not_configured')) %>
22
+ <% elsif set.is_muting %>
23
+ <%= pf_level_badge(level == 'hide' ? 'muted' : level, label: (t('.muted_hidden') if level == 'hide')) %>
24
+ <% else %>
25
+ <%= pf_level_badge(level, strategy: (field.mask_strategy if level == 'masked')) %>
26
+ <% end %>
27
+ <span class="pf-option__aside"><%= pf_people_text(row[:people]) %></span>
28
+ <% end %>
29
+ <% end %>
30
+ </div>
31
+ <% end %>
@@ -0,0 +1,36 @@
1
+ <%# A list of audit events, newest first: when, what (a sentence), who, how.
2
+ Locals: events, lookups; optional empty (text when there are none). %>
3
+ <% if events.empty? %>
4
+ <p class="pf-empty"><%= local_assigns[:empty] || t('permission_forge.audit_log.empty') %></p>
5
+ <% else %>
6
+ <ol class="pf-audit-list" aria-label="<%= t('permission_forge.audit_log.list_label') %>">
7
+ <% events.each do |event| %>
8
+ <li class="pf-audit-event">
9
+ <time class="pf-audit-event__when" datetime="<%= event.occurred_at.utc.iso8601 %>">
10
+ <span class="pf-fw-600"><%= pf_date(event.occurred_at) %></span>
11
+ <span class="pf-text-3"><%= event.occurred_at.in_time_zone.strftime('%H:%M:%S') %></span>
12
+ </time>
13
+ <div class="pf-audit-event__main">
14
+ <%= link_to pf_audit_sentence(event, lookups), pf_audit_event_path(event), class: 'pf-audit-event__sentence' %>
15
+ <div class="pf-audit-event__meta">
16
+ <%= pf_audit_category_badge(event.category) %>
17
+ <%= link_to event.event, pf_audit_log_path(event: event.event), class: 'pf-mono-key',
18
+ title: t('permission_forge.audit_log.filter_event', event: event.event) %>
19
+ <span aria-hidden="true">·</span>
20
+ <%= link_to pf_audit_actor_name(event), pf_audit_log_path(actor_type: event.actor_type, actor_id: event.actor_id),
21
+ title: t('permission_forge.audit_log.filter_actor') %>
22
+ <span aria-hidden="true">·</span>
23
+ <span><%= pf_audit_source_label(event.source) %></span>
24
+ <% if event.reason.present? %>
25
+ <span aria-hidden="true">·</span>
26
+ <span class="pf-audit-event__reason"><%= t('permission_forge.audit_log.reason_inline', reason: event.reason) %></span>
27
+ <% end %>
28
+ </div>
29
+ </div>
30
+ <%= link_to pf_audit_event_path(event), class: 'pf-btn pf-btn--xs pf-audit-event__open' do %>
31
+ <%= t('permission_forge.audit_log.details') %> <span aria-hidden="true">→</span>
32
+ <% end %>
33
+ </li>
34
+ <% end %>
35
+ </ol>
36
+ <% end %>
@@ -0,0 +1,16 @@
1
+ <footer class="pf-audit-footer">
2
+ <% if PermissionForge.configuration.audit_trail_enabled %>
3
+ <% last_event = pf_audit_last_change(record) %>
4
+ <%= prefix %>
5
+ <%= t('.created', created_by: pf_person_name(record.try(:created_by)), created_at: pf_date(record.created_at)) %>
6
+ ·
7
+ <% if last_event %>
8
+ <%= t('.edited', updated_by: pf_audit_actor_name(last_event), updated_at: pf_relative(last_event.occurred_at)) %>
9
+ · <%= link_to t('.history'), pf_audit_log_path(record_type: last_event.record_type, record_id: last_event.record_id) %>
10
+ <% else %>
11
+ <%= t('.edited', updated_by: pf_person_name(record.try(:updated_by)), updated_at: pf_relative(record.updated_at)) %>
12
+ <% end %>
13
+ <% else %>
14
+ <%= t('.disabled') %> <%= pf_setting_link('audit_trail_enabled') %>
15
+ <% end %>
16
+ </footer>
@@ -0,0 +1,15 @@
1
+ <%# Locals: problem (Admin::AuditLog.recording_problem: :off, :failing or :missing_table). %>
2
+ <div class="pf-banner pf-banner--warn" role="note">
3
+ <span class="pf-banner__glyph" aria-hidden="true">⚠</span>
4
+ <div class="pf-banner__body">
5
+ <span class="pf-banner__title"><%= t('permission_forge.audit_log.off.title') %></span>
6
+ <% if problem == :off %>
7
+ <span><%= t('permission_forge.audit_log.off.disabled') %></span>
8
+ <% elsif problem == :failing %>
9
+ <span><%= t('permission_forge.audit_log.off.failing') %></span>
10
+ <% else %>
11
+ <span><%= t('permission_forge.audit_log.off.missing_table') %></span>
12
+ <% end %>
13
+ </div>
14
+ <%= link_to t('permission_forge.ui.open_setting'), pf_setting_path('audit_events'), class: 'pf-btn pf-btn--xs' %>
15
+ </div>
@@ -0,0 +1,19 @@
1
+ <%# Page links for an AuditLog::Page. Locals: page, path (lambda: page number -> path). %>
2
+ <% if page.total.positive? %>
3
+ <nav class="pf-card__footer pf-card__footer--muted pf-pagination" aria-label="<%= t('permission_forge.audit_log.pages_label') %>">
4
+ <span><%= t('permission_forge.audit_log.showing', from: page.first_index, to: page.last_index, count: page.total) %></span>
5
+ <% if page.pages > 1 %>
6
+ <span class="pf-actions">
7
+ <% if page.previous? %>
8
+ <%= link_to t('permission_forge.audit_log.first_page'), path.call(1), class: 'pf-btn pf-btn--xs' %>
9
+ <%= link_to t('permission_forge.ui.previous'), path.call(page.number - 1), class: 'pf-btn pf-btn--xs', rel: 'prev' %>
10
+ <% end %>
11
+ <span class="pf-fs-13"><%= t('permission_forge.audit_log.page_of', page: page.number, pages: page.pages) %></span>
12
+ <% if page.next? %>
13
+ <%= link_to t('permission_forge.ui.next'), path.call(page.number + 1), class: 'pf-btn pf-btn--xs', rel: 'next' %>
14
+ <%= link_to t('permission_forge.audit_log.last_page'), path.call(page.pages), class: 'pf-btn pf-btn--xs' %>
15
+ <% end %>
16
+ </span>
17
+ <% end %>
18
+ </nav>
19
+ <% end %>
@@ -0,0 +1,7 @@
1
+ <div class="pf-banner pf-banner--block" role="alert">
2
+ <span class="pf-banner__glyph" aria-hidden="true">⊘</span>
3
+ <div class="pf-banner__body">
4
+ <span class="pf-banner__title"><%= t('permission_forge.audit_log.refused.title') %></span>
5
+ <span><%= t('permission_forge.audit_log.refused.body') %></span>
6
+ </div>
7
+ </div>
@@ -0,0 +1,29 @@
1
+ <%# A record's or a person's timeline on its audit tab.
2
+ Locals: title, description, page, lookups, path (page number -> path), links ([[label, path], ...]),
3
+ views_path (the same timeline with the views of the log; nil when they are shown). %>
4
+ <section class="pf-card" aria-labelledby="pf-timeline-title">
5
+ <div class="pf-card__header">
6
+ <div class="pf-stack pf-stack--tight pf-grow">
7
+ <h2 class="pf-h2" id="pf-timeline-title"><%= title %></h2>
8
+ <span class="pf-card__desc"><%= description %></span>
9
+ </div>
10
+ <div class="pf-actions">
11
+ <% links.each do |label, href| %>
12
+ <%= link_to label, href, class: 'pf-btn pf-btn--sm' %>
13
+ <% end %>
14
+ </div>
15
+ </div>
16
+ <% if (problem = PermissionForge::Admin::AuditLog.recording_problem) %>
17
+ <div class="pf-card__body">
18
+ <%= render 'permission_forge/shared/audit_off_note', problem: problem %>
19
+ </div>
20
+ <% end %>
21
+ <% if local_assigns[:views_path] %>
22
+ <p class="pf-card__row pf-fs-13 pf-text-2 pf-audit-views-note">
23
+ <%= t('permission_forge.audit_events.index.views_hidden') %>
24
+ <%= link_to t('permission_forge.audit_events.index.views_show'), views_path %>
25
+ </p>
26
+ <% end %>
27
+ <%= render 'permission_forge/shared/audit_events', events: page.events, lookups: lookups %>
28
+ <%= render 'permission_forge/shared/audit_pagination', page: page, path: path %>
29
+ </section>
@@ -0,0 +1,18 @@
1
+ <%# CRUD toggle cell: a checkbox styled as a cell (hidden "0" so unchecking is submitted).
2
+ Locals: name, flag, checked, muting, locked, aria, requires (opt), size (opt), form (opt) %>
3
+ <% form_attr = local_assigns[:form] ? { form: form } : {} %>
4
+ <% requires = local_assigns[:requires] %>
5
+ <label class="pf-cell<%= ' pf-cell--muting' if muting %><%= ' pf-cell--lg' if local_assigns[:size] == :lg %>"
6
+ title="<%= t('.needs_read') if requires && !muting %>">
7
+ <%= tag.input(type: 'hidden', name: name, value: '0', **form_attr) %>
8
+ <%= tag.input(type: 'checkbox', name: name, value: '1', checked: checked, disabled: locked,
9
+ 'aria-label': aria, data: { pf_flag: flag, pf_requires: (muting ? nil : requires), pf_locked: (locked || nil) }, **form_attr) %>
10
+ <% if muting %>
11
+ <span class="pf-cell__on"><span class="pf-cell__glyph" aria-hidden="true">⊘</span><%= t('.muted') %></span>
12
+ <span class="pf-cell__off"><span class="pf-cell__glyph" aria-hidden="true">–</span><%= t('.unchanged') %></span>
13
+ <% else %>
14
+ <span class="pf-cell__on"><span class="pf-cell__glyph" aria-hidden="true">✓</span><%= t('.allowed') %></span>
15
+ <span class="pf-cell__off"><span class="pf-cell__glyph" aria-hidden="true">–</span><%= t('.no') %></span>
16
+ <span class="pf-cell__needs"><span class="pf-cell__glyph" aria-hidden="true">–</span><%= t('.needs_read_short') %></span>
17
+ <% end %>
18
+ </label>
@@ -0,0 +1,11 @@
1
+ <%# Confirmation dialog: a standalone page without JS, a <dialog> with JS. %>
2
+ <% danger = local_assigns.fetch(:danger, false) %>
3
+ <% unless pf_fragment? %>
4
+ <% if local_assigns[:breadcrumb] %><%= breadcrumb %><% end %>
5
+ <% end %>
6
+ <div class="pf-confirm<%= ' pf-confirm--danger' if danger %><%= ' pf-card pf-modal-page' unless pf_fragment? %>"
7
+ data-pf-role="<%= local_assigns.fetch(:role, 'alertdialog') %>" <%= pf_attrs_when(local_assigns[:wide], data: { pf_size: 'wide' }) %>>
8
+ <% if local_assigns[:eyebrow] %><span class="pf-badge pf-badge--block pf-badge--square"><%= eyebrow %></span><% end %>
9
+ <%= content_tag(pf_fragment? ? :h2 : :h1, title, class: 'pf-confirm__title', data: { pf_title: true }) %>
10
+ <%= body %>
11
+ </div>
@@ -0,0 +1,27 @@
1
+ <%# Expiry presets as radio cards. Locals: selected, custom_date, error, name (default 'expiry'). %>
2
+ <% name = local_assigns.fetch(:name, 'expiry') %>
3
+ <% tomorrow = (Time.zone.today + 1).iso8601 %>
4
+ <div class="pf-expiry pf-stack">
5
+ <div class="pf-choices pf-choices--expiry" role="radiogroup" aria-label="<%= t('.label') %>">
6
+ <% PermissionForge::Admin::ExpiryPresets::KEYS.each do |key| %>
7
+ <label class="pf-choice">
8
+ <input type="radio" name="<%= name %>" value="<%= key %>" <%= 'checked' if selected == key %>>
9
+ <span class="pf-choice__box">
10
+ <span class="pf-choice__title"><%= t(".presets.#{key}") %></span>
11
+ <span class="pf-choice__sub">
12
+ <% if key == 'custom' %><%= t('.pick_date') %>
13
+ <% elsif key == 'never' %><%= t('.default') %>
14
+ <% else %><%= pf_expiry_preset_date(key) %>
15
+ <% end %>
16
+ </span>
17
+ </span>
18
+ </label>
19
+ <% end %>
20
+ </div>
21
+ <div class="pf-custom-date pf-field">
22
+ <label class="pf-label" for="pf-custom-date"><%= t('.custom_label') %></label>
23
+ <input id="pf-custom-date" class="pf-input pf-input--auto" type="date" name="custom_date" min="<%= tomorrow %>" value="<%= custom_date %>"
24
+ <%= pf_attrs_when(error, aria: { invalid: true }) %>>
25
+ </div>
26
+ <% if error %><p class="pf-error" role="alert"><%= error %></p><% end %>
27
+ </div>
@@ -0,0 +1,3 @@
1
+ <%# Intentionally empty. Host apps can override this partial (same path in
2
+ their app/views) to add <link rel="stylesheet"> tags that re-theme the
3
+ admin by overriding the --pf-* custom properties on .pf-root. %>
@@ -0,0 +1,10 @@
1
+ <div class="pf-banner pf-banner--help" role="note">
2
+ <div class="pf-banner__body">
3
+ <span><strong><%= term %>:</strong> <%= text %></span>
4
+ </div>
5
+ <%= form_with url: permission_forge.preferences_path, method: :patch, class: 'pf-inline-form' do %>
6
+ <input type="hidden" name="dismiss_help" value="<%= key %>">
7
+ <input type="hidden" name="return_to" value="<%= request.fullpath %>">
8
+ <button type="submit" class="pf-btn pf-btn--xs"><%= t('.got_it') %></button>
9
+ <% end %>
10
+ </div>
@@ -0,0 +1,18 @@
1
+ <%# 4-level field control (radio group styled as a segmented control).
2
+ Locals: name, checked (level or nil), muting, aria, locked, edit_disabled, form (opt), large (opt), id_prefix %>
3
+ <% form_attr = local_assigns[:form] ? { form: form } : {} %>
4
+ <% levels = PermissionForge::Admin::FieldLevel.levels_for(muting: muting) %>
5
+ <div class="pf-levels<%= ' pf-levels--lg' if local_assigns[:large] %>" role="radiogroup" aria-label="<%= aria %>">
6
+ <% levels.each do |level| %>
7
+ <% id = "#{id_prefix}-#{level}" %>
8
+ <% disabled = locked || (level == 'edit' && edit_disabled) %>
9
+ <% title = if locked then t('permission_forge.ui.locked.title')
10
+ elsif level == 'edit' && edit_disabled then t('.needs_edit', resource: local_assigns[:resource_label])
11
+ end %>
12
+ <span class="pf-level pf-level--<%= level %>">
13
+ <%= tag.input(type: 'radio', id: id, name: name, value: level, checked: checked == level, disabled: disabled,
14
+ data: { pf_locked: (locked || nil) }, **form_attr) %>
15
+ <label class="pf-level__label" for="<%= id %>" title="<%= title %>"><%= pf_level_label(level) %></label>
16
+ </span>
17
+ <% end %>
18
+ </div>
@@ -0,0 +1,22 @@
1
+ <%# Muting / Locked notices for set, resource and field pages. %>
2
+ <% if muting %>
3
+ <div class="pf-banner pf-banner--muting" role="note">
4
+ <span class="pf-banner__glyph" aria-hidden="true">⊘</span>
5
+ <div class="pf-banner__body">
6
+ <span class="pf-banner__title"><%= t('.muting_title') %></span>
7
+ <span><%= t('.muting_body') %></span>
8
+ </div>
9
+ </div>
10
+ <% end %>
11
+ <% if pf_locked_record?(record) %>
12
+ <div class="pf-banner" role="note">
13
+ <span class="pf-banner__glyph" aria-hidden="true"><%= pf_lock_icon %></span>
14
+ <div class="pf-banner__body">
15
+ <span class="pf-banner__title"><%= t('permission_forge.ui.locked.title') %></span>
16
+ <span><%= t('.locked_body_html', flag: content_tag(:code, 'is_editable: false')) %></span>
17
+ </div>
18
+ <% if local_assigns[:duplicate_path] %>
19
+ <%= button_to t('.duplicate_to_edit'), duplicate_path, method: :post, class: 'pf-btn pf-btn--sm', form_class: 'pf-inline-form' %>
20
+ <% end %>
21
+ </div>
22
+ <% end %>
@@ -0,0 +1,12 @@
1
+ <% attention = pf_attention.total %>
2
+ <% risks = pf_settings_catalog.risky_rows.size %>
3
+ <div class="pf-nav">
4
+ <%= pf_nav_link t('permission_forge.nav.home'), permission_forge.root_path, exact: true,
5
+ badge: attention, badge_label: t('permission_forge.nav.badge_aria', count: attention) %>
6
+ <%= pf_nav_link t('permission_forge.nav.people'), permission_forge.people_path %>
7
+ <%= pf_nav_link t('permission_forge.nav.permission_sets'), permission_forge.permission_sets_path %>
8
+ <%= pf_nav_link t('permission_forge.nav.groups'), permission_forge.groups_path %>
9
+ <%= pf_nav_link t('permission_forge.nav.audit_log'), permission_forge.audit_events_path %>
10
+ <%= pf_nav_link t('permission_forge.nav.settings'), permission_forge.settings_path,
11
+ badge: risks, badge_label: t('permission_forge.nav.badge_aria', count: risks) %>
12
+ </div>
@@ -0,0 +1,24 @@
1
+ <% locales = pf_available_locales %>
2
+ <% if locales.size > 1 %>
3
+ <div class="pf-stack pf-stack--tight">
4
+ <span class="pf-sidebar__label" aria-hidden="true"><%= t('.language') %></span>
5
+ <div class="pf-segmented" role="group" aria-label="<%= t('.language') %>">
6
+ <% locales.each do |locale| %>
7
+ <%= link_to locale.to_s.upcase, pf_locale_switch_path(locale), class: 'pf-segmented__item',
8
+ lang: locale, title: pf_locale_label(locale), 'aria-current': (I18n.locale.to_s == locale.to_s).to_s %>
9
+ <% end %>
10
+ </div>
11
+ </div>
12
+ <% end %>
13
+ <%= form_with url: permission_forge.preferences_path, method: :patch, class: 'pf-stack pf-stack--tight' do %>
14
+ <span class="pf-sidebar__label" aria-hidden="true"><%= t('.theme') %></span>
15
+ <input type="hidden" name="return_to" value="<%= request.fullpath %>">
16
+ <div class="pf-segmented" role="group" aria-label="<%= t('.theme') %>">
17
+ <% %w[auto light dark].each do |theme| %>
18
+ <button type="submit" name="theme" value="<%= theme %>" class="pf-segmented__item" aria-current="<%= (pf_theme == theme).to_s %>"><%= t(".themes.#{theme}") %></button>
19
+ <% end %>
20
+ </div>
21
+ <% end %>
22
+ <% if pf_current_admin %>
23
+ <span class="pf-sidebar__label"><%= t('.signed_in_as', name: pf_person_name(pf_current_admin)) %></span>
24
+ <% end %>
@@ -0,0 +1,20 @@
1
+ <%# Optional reason for a change, kept on its audit events (and on an
2
+ assignment). Collapsed; works without JS. Locals (all optional):
3
+ id — unique input id on the page; compact — a small toggle for a row's
4
+ inline form; form — the id of the form the input belongs to when it sits
5
+ outside it; value — the reason typed before a re-render (opens the toggle). %>
6
+ <% input_id = local_assigns[:id] || 'pf-reason' %>
7
+ <% compact = local_assigns[:compact] %>
8
+ <% value = local_assigns[:value].is_a?(String) ? local_assigns[:value] : nil %>
9
+ <details class="pf-reason<%= ' pf-reason--compact' if compact %>"<%= ' open' if value.present? %>>
10
+ <summary class="pf-reason__summary">
11
+ <span class="pf-disclosure" aria-hidden="true">›</span>
12
+ <%= compact ? t('permission_forge.shared.reason_field.summary_short') : t('permission_forge.shared.reason_field.summary') %>
13
+ </summary>
14
+ <div class="pf-reason__body">
15
+ <label class="<%= compact ? 'pf-sr-only' : 'pf-label' %>" for="<%= input_id %>"><%= t('permission_forge.shared.reason_field.label') %></label>
16
+ <input id="<%= input_id %>" class="pf-input pf-input--sm" type="text" name="reason" maxlength="255" autocomplete="off" value="<%= value %>"
17
+ placeholder="<%= t('permission_forge.shared.reason_field.placeholder') %>"<%= tag.attributes(form: local_assigns[:form]) if local_assigns[:form] %>>
18
+ <% unless compact %><span class="pf-hint"><%= t('permission_forge.shared.reason_field.hint') %></span><% end %>
19
+ </div>
20
+ </details>
@@ -0,0 +1,13 @@
1
+ <%# Unsaved-changes bar: the submit area of a batched-edit form (form="id"). %>
2
+ <div id="<%= id %>" class="pf-savebar" role="region" aria-label="<%= t('.label') %>" data-pf-dirty="false">
3
+ <div class="pf-savebar__text">
4
+ <span class="pf-savebar__count pf-savebar__count--dirty" data-pf-dirty-count
5
+ data-pf-one="<%= t('.count.one', count: '%{count}') %>" data-pf-other="<%= t('.count.other', count: '%{count}') %>"></span>
6
+ <span class="pf-savebar__count pf-savebar__count--clean"><%= t('.clean') %></span>
7
+ <span class="pf-savebar__note"><%= people.to_i.positive? ? t('.note', people: pf_people_text(people)) : t('.note_nobody') %></span>
8
+ </div>
9
+ <div class="pf-actions">
10
+ <button type="reset" form="<%= form %>" class="pf-btn pf-btn--sm pf-btn--on-dark" hidden data-pf-js-show><%= t('.discard') %></button>
11
+ <button type="submit" form="<%= form %>" class="pf-btn pf-btn--sm pf-btn--inverse"><%= t('.review') %></button>
12
+ </div>
13
+ </div>