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,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Admin
5
+ # Reads RuleForm's inputs, either from the submitted rule-builder params or
6
+ # from a saved PermissionSetRule. Returns a hash keyed by RuleForm reader
7
+ # (name, path, field_name, operator, value, values, value_from, value_to,
8
+ # current_user, include_nulls, failure_message, join, is_active).
9
+ module RuleFormInput
10
+ # @param attrs [Hash{String => Object}] stringified params
11
+ def self.from_params(attrs)
12
+ param_path(attrs).merge(param_values(attrs), param_options(attrs))
13
+ end
14
+
15
+ # @param rule [PermissionSetRule, nil] nil for a new rule
16
+ def self.from_rule(rule)
17
+ rule ||= PermissionForge::PermissionSetRule.new(operator: 'equal', include_nulls: false, is_active: true)
18
+ path = rule_path(rule)
19
+ path.merge(rule_values(rule.field_value.to_s, RuleForm::ARITY.fetch(path[:operator], :one)), rule_options(rule))
20
+ end
21
+
22
+ def self.param_path(attrs)
23
+ path = Array(attrs['path']).map(&:to_s).compact_blank
24
+ remove = attrs['remove_step']
25
+ { name: attrs['name'].to_s.strip, path: remove.present? ? path.first(remove.to_i) : path,
26
+ field_name: attrs['field_name'].to_s, operator: allowed(attrs['operator'], RuleForm::ARITY.keys, 'equal') }
27
+ end
28
+
29
+ def self.param_values(attrs)
30
+ { value: stripped(attrs['value']), values: list_values(attrs),
31
+ value_from: stripped(attrs['value_from']), value_to: stripped(attrs['value_to']),
32
+ current_user: attrs['current_user'] == '1' }
33
+ end
34
+
35
+ # Checked list values plus comma-separated free text.
36
+ def self.list_values(attrs)
37
+ values = Array(attrs['values']) + attrs['values_text'].to_s.split(',')
38
+ values.map { |v| stripped(v) }.compact_blank.uniq
39
+ end
40
+
41
+ def self.param_options(attrs)
42
+ { include_nulls: attrs['include_nulls'] == '1', failure_message: stripped(attrs['failure_message']),
43
+ join: allowed(attrs['join'], RuleForm::JOINS, 'auto'),
44
+ is_active: attrs.key?('is_active') ? attrs['is_active'] == '1' : true }
45
+ end
46
+
47
+ def self.rule_path(rule)
48
+ { name: rule.name.to_s, path: rule.field_accessor_path.to_s.split('.').compact_blank,
49
+ field_name: rule.field_name.to_s, operator: rule.operator.presence || 'equal' }
50
+ end
51
+
52
+ def self.rule_values(raw, arity)
53
+ current_user = raw == RuleForm::CURRENT_USER
54
+ value_from, value_to = arity == :range ? raw.split(',', 2).map(&:strip) : ['', '']
55
+ { current_user: current_user, value: current_user ? '' : raw,
56
+ values: arity == :list ? raw.split(',').map(&:strip) : [], value_from: value_from, value_to: value_to }
57
+ end
58
+
59
+ def self.rule_options(rule)
60
+ { include_nulls: rule.persisted? ? rule.include_nulls : false, failure_message: rule.failure_message.to_s,
61
+ join: rule.join_type.presence || 'auto', is_active: rule.is_active != false }
62
+ end
63
+
64
+ def self.allowed(value, choices, fallback)
65
+ value = value.to_s
66
+ choices.include?(value) ? value : fallback
67
+ end
68
+
69
+ def self.stripped(value)
70
+ value.to_s.strip
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Admin
5
+ # Decides whether a column name looks like personal data, and which mask
6
+ # strategy is recommended. Sources: config.gdpr_field_patterns (pattern =>
7
+ # strategy) and, when filter_parameters_integration is on, the host app's
8
+ # Rails filter_parameters (recommended strategy 'full').
9
+ class SensitiveColumns
10
+ Match = Struct.new(:pattern, :strategy, keyword_init: true)
11
+
12
+ def initialize(configuration: PermissionForge.configuration, filter_parameters: nil)
13
+ @configuration = configuration
14
+ @filter_parameters = filter_parameters
15
+ end
16
+
17
+ # @return [Match, nil]
18
+ def match(column_name)
19
+ name = column_name.to_s
20
+ regex, strategy = @configuration.gdpr_field_patterns.find { |pattern, _| name.match?(pattern) }
21
+ return Match.new(pattern: pattern_label(regex, name), strategy: strategy) if regex
22
+ return Match.new(pattern: 'filter_parameters', strategy: 'full') if filtered_parameter?(name)
23
+
24
+ nil
25
+ end
26
+
27
+ def sensitive?(column_name)
28
+ !match(column_name).nil?
29
+ end
30
+
31
+ private
32
+
33
+ # Human-ish pattern name for "Matches privacy pattern "email"": the
34
+ # alternative of the regex that actually matched, stripped of anchors.
35
+ def pattern_label(regex, name)
36
+ source = regex.source
37
+ hit = matching_alternative(source, name) || source.split('|').first
38
+ hit.to_s.gsub(/\\[Azb]|\.\*|\[[^\]]*\]/, '').delete('^a-z0-9_').delete_prefix('_').delete_suffix('_').presence || source
39
+ end
40
+
41
+ def matching_alternative(source, name)
42
+ source.split('|').find { |alt| safe_match?(alt, name) }
43
+ end
44
+
45
+ def safe_match?(source, name)
46
+ name.match?(Regexp.new(source))
47
+ rescue RegexpError
48
+ false
49
+ end
50
+
51
+ def filtered_parameter?(name)
52
+ filters = host_filter_parameters
53
+ return false if filters.empty?
54
+
55
+ @parameter_filter ||= ActiveSupport::ParameterFilter.new(filters)
56
+ @parameter_filter.filter(name => 'x')[name] != 'x'
57
+ end
58
+
59
+ def host_filter_parameters
60
+ return Array(@filter_parameters) if @filter_parameters
61
+ return [] unless @configuration.filter_parameters_integration
62
+ return [] unless defined?(Rails) && Rails.application
63
+
64
+ Array(Rails.application.config.filter_parameters)
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,261 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Admin
5
+ # Read-only description of every configuration option for the Settings
6
+ # page: section, value type, current vs default value, and security/privacy
7
+ # risk. Friendly names and explanations live in the locale files under
8
+ # permission_forge.settings.items.<key>.
9
+ #
10
+ # Values are never mutated here. Secrets are reported only as set/not set,
11
+ # and code hooks (lambdas) only as default/custom/not set — never source.
12
+ class SettingsCatalog # rubocop:disable Metrics/ClassLength -- DEFINITIONS is one line per configuration option
13
+ Definition = Struct.new(:key, :section, :type, :options, :unit, :nil_label, :example, :ladder,
14
+ keyword_init: true)
15
+
16
+ Row = Struct.new(:definition, :value, :default, :hook_state, :risk, keyword_init: true) do
17
+ def key = definition.key
18
+ def type = definition.type
19
+ def changed? = value != default
20
+ def warn? = risk == :warn
21
+ def ok? = risk == :ok
22
+
23
+ def recommended = RECOMMENDED[key]
24
+
25
+ # The recommended value (risky rows) or the example (hooks, secrets,
26
+ # names); nil when the snippet shows the current value.
27
+ def snippet_override
28
+ (recommended if warn?) || (definition.example if %i[hook secret names].include?(type))
29
+ end
30
+ end
31
+
32
+ SECTIONS = %w[preset admin_access people field_protection masked_attributes record_rules controller_actions
33
+ activation_conditions privacy caching audit_trail multi_tenancy admin_api admin_ui database].freeze
34
+
35
+ def self.d(key, section, type, **) = Definition.new(key: key.to_s, section: section, type: type, **)
36
+
37
+ DEFINITIONS = [
38
+ d(:preset, 'preset', :enum, options: Configuration::Presets::PRESETS.keys),
39
+
40
+ d(:http_basic_auth_enabled, 'admin_access', :bool),
41
+ d(:http_basic_auth_user, 'admin_access', :secret,
42
+ example: 'Rails.application.credentials.dig(:permission_forge, :username)'),
43
+ d(:http_basic_auth_password, 'admin_access', :secret,
44
+ example: 'Rails.application.credentials.dig(:permission_forge, :password)'),
45
+ d(:base_controller_class, 'admin_access', :class, example: "'::AdminController'"),
46
+ d(:admin_assignment_authorization, 'admin_access', :hook,
47
+ example: '->(controller, target, user_ids, action) { controller.current_user&.admin? }'),
48
+
49
+ d(:user_class, 'people', :class, example: "'User'"),
50
+ d(:current_user_resolver, 'people', :hook, example: '-> { ::Current.user if defined?(::Current) && ::Current.respond_to?(:user) }'),
51
+ d(:user_active_check, 'people', :hook, example: '->(user) { user.active? }'),
52
+ d(:user_list_resolver, 'people', :hook, nil_label: :default, example: '-> { User.order(:name) }'),
53
+ d(:user_list_limit, 'people', :num, unit: :people),
54
+ d(:user_label_formatter, 'people', :hook, nil_label: :default,
55
+ example: "->(user) { \"\#{user.name} <\#{user.email}>\" }"),
56
+ d(:profile_builder, 'people', :hook, example: '->(user) { { id: user.id, name: user.name } }'),
57
+
58
+ d(:unconfigured_field_strategy, 'field_protection', :enum, options: %i[block pass_through warn mask_all]),
59
+ d(:response_filter_nil_user_strategy, 'field_protection', :enum, options: %i[bypass block]),
60
+ d(:custom_mask_strategies, 'field_protection', :names,
61
+ example: "{ 'iban_short' => ->(value) { value[0, 4] + ' **** ' + value[-4..] } }"),
62
+ d(:ignored_fields, 'field_protection', :list),
63
+ d(:field_variant_mappings, 'field_protection', :map),
64
+ d(:extra_fields_method, 'field_protection', :str),
65
+ d(:extra_fields_default_access, 'field_protection', :enum, options: %i[grant unconfigured]),
66
+ d(:virtual_field_discovery, 'field_protection', :bool),
67
+ d(:on_field_filtered, 'field_protection', :hook,
68
+ example: '->(resource_name:, field_name:, action:, user:) { Rails.logger.info(...) }'),
69
+
70
+ d(:auto_apply_masked_attributes, 'masked_attributes', :enum, options: %i[auto list off]),
71
+ d(:masked_write_protection, 'masked_attributes', :enum, options: %i[skip raise off]),
72
+ d(:masked_attributes_nil_user_strategy, 'masked_attributes', :enum, options: %i[bypass hide]),
73
+ d(:hidden_attribute_value, 'masked_attributes', :str, nil_label: :nil),
74
+ d(:omit_hidden_in_attributes_hash, 'masked_attributes', :bool),
75
+ d(:mask_virtual_attributes, 'masked_attributes', :enum, options: %i[off auto list]),
76
+
77
+ d(:nil_user_strategy, 'record_rules', :enum, options: %i[bypass deny raise]),
78
+ d(:on_rule_bypass, 'record_rules', :hook, example: '->(user:, resource:, reason:) { Rails.logger.info(...) }'),
79
+ d(:auto_query_scoping, 'record_rules', :enum, options: %i[off filter_only], ladder: 'enforce'),
80
+ d(:unscoped_query_detection, 'record_rules', :enum, options: %i[off log raise], ladder: 'detect'),
81
+ d(:unscoped_query_detection_environments, 'record_rules', :list),
82
+ d(:database_row_level_security, 'record_rules', :bool, ladder: 'database'),
83
+ d(:rls_session_variable, 'record_rules', :str),
84
+
85
+ d(:controller_action_discovery, 'controller_actions', :bool),
86
+ d(:controller_action_resolver, 'controller_actions', :hook,
87
+ nil_label: :default, example: "->(path, action) { ['Customer', 'merge'] if path == 'customers/merges' }"),
88
+ d(:controller_discovery_exclude, 'controller_actions', :list),
89
+ d(:unconfigured_action_strategy, 'controller_actions', :enum, options: %i[allow block]),
90
+ d(:action_guard_bypass, 'controller_actions', :hook, example: '->(user, _controller) { user.admin? }'),
91
+ d(:action_guard_nil_user_strategy, 'controller_actions', :enum, options: %i[bypass deny], nil_label: :default),
92
+
93
+ d(:request_context_resolver, 'activation_conditions', :hook,
94
+ example: '-> { { ip: Current.ip, session_flags: Current.session_flags, time: Time.current } }'),
95
+ d(:conditional_sets_strict_mode, 'activation_conditions', :bool),
96
+
97
+ d(:gdpr_defaults, 'privacy', :bool),
98
+ d(:gdpr_require_explicit_choice, 'privacy', :bool),
99
+ d(:gdpr_field_patterns, 'privacy', :map),
100
+ d(:filter_parameters_integration, 'privacy', :bool),
101
+
102
+ d(:cache_backend, 'caching', :enum, options: %i[memory rails_cache null]),
103
+ d(:cache_key_prefix, 'caching', :str),
104
+ d(:cache_ttl, 'caching', :num, unit: :seconds),
105
+ d(:conditional_set_cache_ttl, 'caching', :num, unit: :seconds),
106
+ d(:snapshot_transactions, 'caching', :enum, options: %i[auto true false]),
107
+ d(:on_cache_failure, 'caching', :hook,
108
+ example: "->(operation:, user_id:, keys:, error:, **) { Sentry.capture_message(\"permission cache: \#{operation} failed\") }"),
109
+
110
+ d(:audit_trail_enabled, 'audit_trail', :bool),
111
+ d(:audit_user_class, 'audit_trail', :class, example: "'User'"),
112
+ d(:audit_events, 'audit_trail', :enum, options: %i[auto true false]),
113
+ d(:audit_actor_resolver, 'audit_trail', :hook, nil_label: :default,
114
+ example: '->(controller) { controller.current_user }'),
115
+ d(:audit_request_context, 'audit_trail', :hook, nil_label: :default,
116
+ example: '->(controller) { { request_id: controller.request.request_id, ' \
117
+ 'ip: controller.request.remote_ip } }'),
118
+ d(:audit_change_failure_strategy, 'audit_trail', :enum, options: %i[raise report log]),
119
+ d(:audit_hook_failure_strategy, 'audit_trail', :enum, options: %i[report log raise]),
120
+ d(:audit_runtime_events, 'audit_trail', :list),
121
+ d(:audit_log_subscriber, 'audit_trail', :bool),
122
+ d(:audit_retention_period, 'audit_trail', :num, unit: :seconds, nil_label: :nil),
123
+ d(:audit_log_authorization, 'audit_trail', :hook, nil_label: :default,
124
+ example: '->(controller) { controller.current_user&.auditor? }'),
125
+
126
+ d(:tenant_model, 'multi_tenancy', :class, nil_label: :disabled, example: "'Account'"),
127
+ d(:tenant_column, 'multi_tenancy', :str, nil_label: :disabled),
128
+
129
+ d(:api_enabled, 'admin_api', :bool),
130
+ d(:api_auth, 'admin_api', :hook, example: '->(controller) { controller.current_user&.admin? }'),
131
+ d(:api_base_controller_class, 'admin_api', :class, example: "'::Api::BaseController'"),
132
+ d(:api_action_authorization, 'admin_api', :hook,
133
+ example: '->(controller, action, record) { controller.current_user&.admin? }'),
134
+ d(:api_assignment_authorization, 'admin_api', :hook,
135
+ example: '->(controller, permission_set, user_ids) { controller.current_user&.admin? }'),
136
+ d(:api_require_authorization, 'admin_api', :bool),
137
+ d(:api_stats_require_admin, 'admin_api', :bool),
138
+ d(:api_record_identifier, 'admin_api', :enum, options: %i[uuid id]),
139
+ d(:api_default_per_page, 'admin_api', :num, unit: :records),
140
+ d(:api_max_per_page, 'admin_api', :num, unit: :records),
141
+ d(:api_pagination_handler, 'admin_api', :hook, nil_label: :default, example: '->(scope, page, per_page) { scope.page(page).per(per_page) }'),
142
+ d(:api_serializer_overrides, 'admin_api', :map),
143
+ d(:api_redact_rule_field_values, 'admin_api', :bool),
144
+
145
+ d(:admin_model_resolver, 'admin_ui', :hook, nil_label: :default, example: '-> { [Customer, Invoice] }'),
146
+ d(:admin_session_flags, 'admin_ui', :map),
147
+ d(:admin_stale_assignment_days, 'admin_ui', :num, unit: :days),
148
+ d(:admin_app_name, 'admin_ui', :str, nil_label: :default),
149
+ d(:admin_back_url, 'admin_ui', :hook, nil_label: :default, example: "'/dashboard'"),
150
+ d(:admin_docs_url, 'admin_ui', :str),
151
+ d(:admin_tenantless_access, 'multi_tenancy', :enum, options: %i[deny allow]),
152
+
153
+ d(:table_name_prefix, 'database', :str),
154
+ d(:uuid_generator, 'database', :hook, example: '-> { SecureRandom.uuid }'),
155
+ d(:available_locales, 'database', :list, nil_label: :default)
156
+ ].freeze
157
+
158
+ # Risk rules: key => ->(config) { :warn | :ok | nil }. Recommended values
159
+ # (shown in the "How to change" snippet for risky rows) live in RECOMMENDED.
160
+ RISKS = {
161
+ 'preset' => ->(config) { config.preset == :strict ? :ok : nil },
162
+ 'http_basic_auth_enabled' => lambda { |config|
163
+ if config.http_basic_auth_enabled
164
+ config.http_basic_auth_user.present? && config.http_basic_auth_password.present? ? :ok : :warn
165
+ else
166
+ default_controller?(config.base_controller_class) ? :warn : :ok
167
+ end
168
+ },
169
+ 'conditional_sets_strict_mode' => ->(config) { config.conditional_sets_strict_mode ? :ok : :warn },
170
+ 'admin_tenantless_access' => lambda { |config|
171
+ next nil unless config.tenant_enabled?
172
+
173
+ config.admin_tenantless_access_allowed? ? :warn : :ok
174
+ },
175
+ 'api_require_authorization' => lambda { |config|
176
+ next nil unless config.api_enabled
177
+ next :ok if config.api_require_authorization
178
+ next :ok if config.api_action_authorization && config.api_assignment_authorization
179
+
180
+ :warn
181
+ },
182
+ 'unconfigured_field_strategy' => ->(config) { config.unconfigured_field_strategy.to_s == 'pass_through' ? :warn : nil },
183
+ 'masked_write_protection' => ->(config) { config.masked_write_protection.to_s == 'off' ? :warn : :ok },
184
+ 'audit_trail_enabled' => ->(config) { config.audit_trail_enabled ? :ok : :warn },
185
+ 'audit_events' => ->(config) { AuditLog.recording_problem(config) ? :warn : :ok },
186
+ 'gdpr_defaults' => ->(config) { config.gdpr_defaults ? :ok : nil },
187
+ 'nil_user_strategy' => ->(config) { config.nil_user_strategy.to_s == 'bypass' ? nil : :ok },
188
+ 'masked_attributes_nil_user_strategy' => ->(config) { config.hide_masked_attributes_without_user? ? :ok : nil },
189
+ 'extra_fields_default_access' => ->(config) { config.grant_extra_fields_by_default? ? nil : :ok },
190
+ 'unconfigured_action_strategy' => ->(config) { config.allow_unconfigured_actions? ? nil : :ok },
191
+ 'action_guard_nil_user_strategy' => ->(config) { config.action_guard_nil_user_bypass? ? nil : :ok }
192
+ }.freeze
193
+
194
+ RECOMMENDED = {
195
+ 'conditional_sets_strict_mode' => 'true',
196
+ 'api_require_authorization' => 'true',
197
+ 'admin_tenantless_access' => ':deny',
198
+ 'unconfigured_field_strategy' => ':block',
199
+ 'masked_write_protection' => ':skip',
200
+ 'audit_trail_enabled' => 'true',
201
+ 'audit_events' => ':auto',
202
+ 'http_basic_auth_enabled' => 'true'
203
+ }.freeze
204
+
205
+ def self.default_controller?(name)
206
+ name.to_s.delete_prefix('::') == 'ActionController::Base'
207
+ end
208
+
209
+ def initialize(configuration: PermissionForge.configuration, values: SettingsValues.new)
210
+ @configuration = configuration
211
+ @defaults = Configuration.new
212
+ @values = values
213
+ end
214
+
215
+ def rows
216
+ @rows ||= DEFINITIONS.map { |definition| build_row(definition) }
217
+ end
218
+
219
+ def row(key)
220
+ rows.find { |r| r.key == key.to_s }
221
+ end
222
+
223
+ # [[section, [Row]]] in display order.
224
+ def sections
225
+ rows.group_by { |row| row.definition.section }.slice(*SECTIONS).to_a
226
+ end
227
+
228
+ def risky_rows
229
+ rows.select(&:warn?)
230
+ end
231
+
232
+ def secure_count
233
+ rows.count(&:ok?)
234
+ end
235
+
236
+ def recommended_value(key)
237
+ row(key)&.recommended
238
+ end
239
+
240
+ # Ruby source for the right-hand side of `config.<key> = ...` in the
241
+ # "How to change" snippet: the recommended value for risky rows,
242
+ # otherwise the current value (or an example for hooks and secrets).
243
+ def snippet_value(row)
244
+ row.snippet_override || @values.ruby_literal(read(@configuration, row.key))
245
+ end
246
+
247
+ private
248
+
249
+ def build_row(definition)
250
+ key = definition.key
251
+ value = @values.normalize(definition, read(@configuration, key))
252
+ Row.new(definition: definition, value: value, default: @values.normalize(definition, read(@defaults, key)),
253
+ hook_state: (value if definition.type == :hook), risk: RISKS[key]&.call(@configuration))
254
+ end
255
+
256
+ def read(config, key)
257
+ config.respond_to?(key) ? config.public_send(key) : nil
258
+ end
259
+ end
260
+ end
261
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Admin
5
+ # Turns raw configuration values into the comparable, display-ready values
6
+ # SettingsCatalog shows, and into Ruby source for the "How to change"
7
+ # snippet. Never exposes secrets or lambda bodies.
8
+ class SettingsValues
9
+ # Normalizer per Definition#type; any other type is shown as-is.
10
+ NORMALIZERS = {
11
+ secret: :secret_set?, hook: :hook_state, enum: :enum_value, names: :names_value,
12
+ list: :list_value, map: :map_value, class: :class_value, num: :number_value
13
+ }.freeze
14
+
15
+ # Per-key enum readings that differ from value.to_s (nil = no override).
16
+ # false/nil read as 'off', an Array as 'list'.
17
+ OFF_AUTO_LIST = lambda { |value|
18
+ next 'list' if value.is_a?(Array)
19
+
20
+ 'off' if [false, nil].include?(value)
21
+ }
22
+
23
+ ENUM_OVERRIDES = {
24
+ 'auto_apply_masked_attributes' => OFF_AUTO_LIST,
25
+ 'mask_virtual_attributes' => OFF_AUTO_LIST,
26
+ 'audit_events' => :to_s.to_proc,
27
+ 'cache_backend' => ->(value) { value.class.name unless value.is_a?(Symbol) || value == true }
28
+ }.freeze
29
+
30
+ VARIANT_SAMPLES = %w[owner_id amount_cents customer_uuid price_in_cents created_on].freeze
31
+
32
+ # Comparable, display-ready values. Never exposes secrets or lambda bodies.
33
+ def normalize(definition, value)
34
+ normalizer = NORMALIZERS[definition.type]
35
+ normalizer ? send(normalizer, definition, value) : value
36
+ end
37
+
38
+ # Ruby source for a raw configuration value.
39
+ def ruby_literal(value)
40
+ case value
41
+ when nil then 'nil'
42
+ when Symbol then ":#{value}"
43
+ when String then "'#{value}'"
44
+ when Array then array_literal(value)
45
+ when Hash then '{ ... }'
46
+ when ActiveSupport::Duration then duration_literal(value)
47
+ else value.inspect
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def secret_set?(_definition, value)
54
+ value.present?
55
+ end
56
+
57
+ # A duration (90.days) reads as its seconds.
58
+ def number_value(_definition, value)
59
+ value.is_a?(ActiveSupport::Duration) ? value.to_i : value
60
+ end
61
+
62
+ # 90.days, 1.day + 12.hours
63
+ def duration_literal(value)
64
+ value.parts.map { |unit, count| "#{count}.#{unit}" }.join(' + ')
65
+ end
66
+
67
+ def names_value(_definition, value)
68
+ (value.try(:keys) || Array(value)).map(&:to_s)
69
+ end
70
+
71
+ def list_value(_definition, value)
72
+ value.nil? ? nil : Array(value).map { |item| pattern_text(item) }
73
+ end
74
+
75
+ def class_value(_definition, value)
76
+ value&.to_s&.delete_prefix('::')
77
+ end
78
+
79
+ def enum_value(definition, value)
80
+ override = ENUM_OVERRIDES[definition.key]&.call(value)
81
+ return override if override
82
+
83
+ value == true ? 'auto' : value.to_s
84
+ end
85
+
86
+ def map_value(definition, value)
87
+ return [] if value.blank?
88
+
89
+ key = definition.key
90
+ value.map { |pattern, target| [pattern_text(pattern), map_target(key, pattern, target)] }
91
+ end
92
+
93
+ def pattern_text(pattern)
94
+ pattern.is_a?(Regexp) ? "/#{pattern.source}/" : pattern.to_s
95
+ end
96
+
97
+ def map_target(key, pattern, target)
98
+ describe_target(pattern, target)
99
+ rescue StandardError
100
+ key == 'field_variant_mappings' ? 'custom code' : target.to_s
101
+ end
102
+
103
+ # Plain values as text; callables as "sample → result" on a sample
104
+ # column name the pattern matches, else 'custom code'.
105
+ def describe_target(pattern, target)
106
+ return target.to_s unless target.respond_to?(:call)
107
+ return target.name.to_s if target.is_a?(Class)
108
+
109
+ sample = VARIANT_SAMPLES.find { |candidate| candidate.match?(pattern) } if pattern.is_a?(Regexp)
110
+ sample ? "#{sample} → #{target.call(sample)}" : 'custom code'
111
+ end
112
+
113
+ # :default (gem's own lambda, or nil where nil means built-in behaviour),
114
+ # :custom (host lambda), :none (nil with no fallback).
115
+ def hook_state(definition, value)
116
+ return (definition.nil_label == :default ? :default : :none) if value.nil?
117
+
118
+ file = value.respond_to?(:source_location) ? value.source_location&.first.to_s : ''
119
+ file.include?('lib/permission_forge/configuration') ? :default : :custom
120
+ end
121
+
122
+ def array_literal(value)
123
+ return "%w[#{value.join(' ')}]" if value.all?(String)
124
+ return "%i[#{value.join(' ')}]" if value.all?(Symbol)
125
+
126
+ value.inspect
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Api
5
+ module Serializers
6
+ # An audit event as the API shows it: every column but the tenant column
7
+ # (internal), `data` as an object, times in ISO 8601 (UTC, microseconds).
8
+ class AuditEventSerializer < BaseSerializer
9
+ TIME_ATTRIBUTES = %i[occurred_at created_at].freeze
10
+
11
+ attributes :id, :uuid, :occurred_at, :event, :category,
12
+ :actor_type, :actor_id, :actor_label, :source,
13
+ :record_type, :record_id, :record_uuid, :record_label,
14
+ :permission_set_id, :permission_set_uuid, :subject_user_id,
15
+ :data, :reason, :request_id, :ip, :correlation_id, :created_at
16
+
17
+ TIME_ATTRIBUTES.each do |name|
18
+ define_method(name) { record.public_send(name)&.utc&.iso8601(6) }
19
+ end
20
+
21
+ def data
22
+ record.data.presence || {}
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ # @api private
5
+ module Api
6
+ module Serializers
7
+ class BaseSerializer
8
+ def initialize(record)
9
+ @record = record
10
+ end
11
+
12
+ def as_json(*)
13
+ self.class.attribute_names.to_h do |attr|
14
+ [attr, respond_to?(attr, true) ? send(attr) : @record.public_send(attr)]
15
+ end
16
+ end
17
+
18
+ def self.collection(records)
19
+ records.map { |r| new(r).as_json }
20
+ end
21
+
22
+ def self.attributes(*attrs)
23
+ @attribute_names = attrs
24
+ end
25
+
26
+ def self.attribute_names
27
+ @attribute_names || []
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :record
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Api
5
+ module Serializers
6
+ class PermissionSetGroupSerializer < BaseSerializer
7
+ attributes :id, :uuid, :name, :description, :is_active, :is_editable,
8
+ :created_at, :updated_at
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Api
5
+ module Serializers
6
+ class PermissionSetGroupUserSerializer < BaseSerializer
7
+ attributes :id, :uuid, :permission_set_group_id, :user_id, :created_at, :updated_at
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Api
5
+ module Serializers
6
+ class PermissionSetRuleSerializer < BaseSerializer
7
+ REDACTED_PLACEHOLDER = '[REDACTED]'
8
+
9
+ attributes :id, :uuid, :permission_set_id, :resource_id,
10
+ :name, :failure_message, :join_type,
11
+ :field_name, :field_value, :field_accessor_path,
12
+ :operator, :include_nulls, :is_active, :is_editable,
13
+ :created_at, :updated_at
14
+
15
+ private
16
+
17
+ def field_value
18
+ return REDACTED_PLACEHOLDER if PermissionForge.configuration.api_redact_rule_field_values
19
+
20
+ record.field_value
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Api
5
+ module Serializers
6
+ class PermissionSetSerializer < BaseSerializer
7
+ attributes :id, :uuid, :name, :description, :is_active, :is_editable, :is_muting,
8
+ :requires_session_flag, :allowed_ips, :allowed_hours_start, :allowed_hours_end,
9
+ :permission_set_group_id, :created_at, :updated_at
10
+ end
11
+ end
12
+ end
13
+ end