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,220 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Api
5
+ class BaseController < PermissionForge.configuration.api_base_controller_class.constantize
6
+ include PermissionForge::ApiAuthentication
7
+ include PermissionForge::TenantRequired
8
+ include PermissionForge::ParamShapes
9
+ include PermissionForge::Api::ResponseRenderer
10
+ include PermissionForge::Api::Pagination
11
+ include PermissionForge::Api::SerializerResolver
12
+
13
+ # API-safe CSRF posture. A mutating request without a valid CSRF token
14
+ # gets an empty session and an empty cookie jar (`:null_session`), so a
15
+ # cross-site form post from a signed-in admin's browser reaches api_auth
16
+ # (and any host `current_user`) with no session: a session- or
17
+ # cookie-based api_auth refuses it. Clients that authenticate with a
18
+ # header (a token, an API key) are unaffected. `prepend: true` puts the
19
+ # check first in the chain, before verify_api_enabled! /
20
+ # authenticate_api_request! (ApiAuthentication) and before the host
21
+ # base controller's own callbacks — registered after them it would
22
+ # only empty the session once api_auth had already let the request in.
23
+ # ActionController::API bases have no forgery protection (and no
24
+ # cookies), so this is a no-op there.
25
+ # Verified like the admin screens (ForgeryProtection).
26
+ include PermissionForge::ForgeryProtection
27
+
28
+ protect_from_forgery with: :null_session, prepend: true, **pf_forgery_strategy if respond_to?(:protect_from_forgery)
29
+
30
+ AUDIT_SOURCE = 'api'
31
+ # Largest id a bigint column holds; past it PostgreSQL raises instead of matching nothing.
32
+ MAX_RECORD_ID = (2**63) - 1
33
+
34
+ before_action :set_api_locale
35
+ before_action :pf_require_tenant
36
+ # After api_auth (a refused request records its own event).
37
+ around_action :pf_audit_request
38
+
39
+ rescue_from ActiveRecord::RecordNotFound do
40
+ render json: { error: { status: 404, messages: I18n.t('permission_forge.api.errors.not_found') } }, status: :not_found
41
+ end
42
+
43
+ rescue_from ActiveRecord::RecordInvalid do |e|
44
+ render json: { error: { status: 422, messages: e.record.errors.messages } }, status: :unprocessable_entity
45
+ end
46
+
47
+ rescue_from ActionController::ParameterMissing, ActionController::BadRequest do |e|
48
+ render json: { error: { status: 422, messages: e.message } }, status: :unprocessable_entity
49
+ end
50
+
51
+ private
52
+
53
+ # Who acts (Audit::ActorResolver: audit_actor_resolver, the signed-in
54
+ # user, else an anonymous API client), request id, IP, the `reason`
55
+ # body key and one correlation id per mutating request.
56
+ def pf_audit_request(&)
57
+ PermissionForge::Audit::RequestContext.wrap(self, source: AUDIT_SOURCE, &)
58
+ end
59
+
60
+ # Records a refused request so that no rollback takes it along.
61
+ def pf_audit_refused(event, **payload)
62
+ PermissionForge::Audit::RequestContext.refused(self, event, source: AUDIT_SOURCE, **payload)
63
+ end
64
+
65
+ def set_api_locale
66
+ locale = params[:locale] || extract_locale_from_accept_language
67
+ # Always assign a locale (default when none/invalid) so a locale set by
68
+ # one request can never leak into the next on a reused thread.
69
+ I18n.locale = if locale.present? && api_available_locale?(locale)
70
+ locale
71
+ else
72
+ I18n.default_locale
73
+ end
74
+ end
75
+
76
+ def extract_locale_from_accept_language
77
+ header = request.env['HTTP_ACCEPT_LANGUAGE']
78
+ return nil if header.blank?
79
+
80
+ # Split on commas, drop q-weights, take the primary subtag of each tag.
81
+ # Matches bare tags ("en") as well as region/quality forms ("en-US;q=0.8").
82
+ header.split(',').filter_map do |part|
83
+ tag = part.split(';').first.to_s.strip
84
+ primary = tag.split('-').first&.downcase
85
+ primary if primary && api_available_locale?(primary)
86
+ end.first
87
+ end
88
+
89
+ def api_available_locale?(locale)
90
+ allowed = PermissionForge.configuration.available_locales || PermissionForge::BUNDLED_LOCALES
91
+ Array(allowed).map(&:to_s).include?(locale.to_s)
92
+ end
93
+
94
+ # The record `params[param_key]` names in `scope` — the one lookup every
95
+ # API endpoint uses. Under api_record_identifier = :id only digits name a
96
+ # record: the database would cast '1abc', or a uuid that starts with
97
+ # digits, to 1 and answer for another record. A NUL byte names nothing
98
+ # (PostgreSQL raises on it). Raises RecordNotFound, rendered as a 404.
99
+ def find_record(scope, param_key = :identifier)
100
+ identifier = PermissionForge.configuration.api_record_identifier
101
+ value = params[param_key].to_s
102
+ raise ActiveRecord::RecordNotFound unless identifier_value?(identifier, value)
103
+
104
+ scope.find_by!(identifier => value)
105
+ end
106
+
107
+ def identifier_value?(identifier, value)
108
+ return false if value.empty? || value.include?("\0")
109
+ return true unless identifier.to_s == 'id'
110
+
111
+ value.match?(/\A[1-9]\d{0,18}\z/) && value.to_i <= MAX_RECORD_ID
112
+ end
113
+
114
+ # Looks up a host user by id within config.user_list_resolver
115
+ # (Admin::PeopleDirectory#find), rendering a 404 (and returning nil)
116
+ # when the id names nobody there — a missing user, someone the
117
+ # resolver leaves out, or a value that is not a key.
118
+ def find_host_user(user_id)
119
+ user = PermissionForge::Admin::PeopleDirectory.new.find(user_id)
120
+ render_not_found unless user
121
+ user
122
+ end
123
+
124
+ def authorize_action!(action, record)
125
+ authorizer = PermissionForge.configuration.api_action_authorization
126
+ message = I18n.t('permission_forge.api.errors.action_not_authorized')
127
+ unless authorizer
128
+ return deny_when_authorization_required(message) do
129
+ audit_action_refused(action, record, hook: 'api_require_authorization')
130
+ end
131
+ end
132
+
133
+ return if authorizer.call(self, action, record)
134
+
135
+ audit_action_refused(action, record, hook: 'api_action_authorization')
136
+ render_not_authorized(message)
137
+ end
138
+
139
+ # Runs the mutation authorization hook and the is_editable lock for a
140
+ # create/update/destroy. May render a 403; callers follow with
141
+ # `return if performed?`. Used by CrudActions and the bespoke controllers
142
+ # so every mutation path is guarded consistently.
143
+ # A destroy also asks the record's container (EditableLock.container_of):
144
+ # a set in a locked group is not deleted.
145
+ def guard_mutation!(action, record)
146
+ authorize_action!(action, record)
147
+ return if performed?
148
+
149
+ enforce_editable!(record, (PermissionForge::Admin::EditableLock.container_of(record) if action == :destroy))
150
+ end
151
+
152
+ # Blocks a change when a record, or what it belongs to (its Resource,
153
+ # Permission Set, or the Group of a membership — Admin::EditableLock),
154
+ # is flagged is_editable: false: a new child of a locked parent is
155
+ # refused like a change to the parent. Records with a nil/true flag are
156
+ # unaffected. The refusal names the locked record.
157
+ def enforce_editable!(*records)
158
+ reject_locked!(PermissionForge::Admin::EditableLock.locked_record(*records))
159
+ end
160
+
161
+ def reject_locked!(locked)
162
+ return unless locked
163
+
164
+ pf_audit_refused('admin.lock_refused', record: locked)
165
+ render json: {
166
+ error: { status: 403, messages: I18n.t('permission_forge.api.errors.not_editable') }
167
+ }, status: :forbidden
168
+ end
169
+
170
+ # config.api_assignment_authorization receives (controller, target,
171
+ # user_ids): the Permission Set (or the Group of a membership) and an
172
+ # Array of the people concerned cast to the user model's primary-key
173
+ # type (Integers for integer keys) — whatever shape the request sent.
174
+ # A refusal records the ids as requested (bounded, Audit::Refusals).
175
+ def authorize_assignment!(permission_set, user_ids)
176
+ authorizer = PermissionForge.configuration.api_assignment_authorization
177
+ message = I18n.t('permission_forge.api.errors.assignment_not_authorized')
178
+ unless authorizer
179
+ return deny_when_authorization_required(message) do
180
+ audit_assignment_refused(permission_set, user_ids, hook: 'api_require_authorization')
181
+ end
182
+ end
183
+
184
+ return if authorizer.call(self, permission_set, PermissionForge::Admin::PeopleDirectory::Ids.cast(user_ids))
185
+
186
+ audit_assignment_refused(permission_set, user_ids, hook: 'api_assignment_authorization')
187
+ render_not_authorized(message)
188
+ end
189
+
190
+ # When config.api_require_authorization is on, a missing authorization lambda
191
+ # means DENY (403) rather than the backward-compatible silent allow. This
192
+ # prevents "authenticated == full admin" when the host wired api_auth but not
193
+ # the per-action / assignment authorization hooks. The block records the
194
+ # refusal.
195
+ def deny_when_authorization_required(message)
196
+ return unless PermissionForge.configuration.api_require_authorization
197
+
198
+ yield
199
+ render_not_authorized(message)
200
+ end
201
+
202
+ # api.auth_failed for a mutation the authorization refused. A record
203
+ # not saved yet (create) is named by its class only.
204
+ def audit_action_refused(action, record, hook:)
205
+ reference = if record.respond_to?(:persisted?) && record.persisted? then { record: record }
206
+ elsif record then { record_type: record.class.name }
207
+ else {}
208
+ end
209
+ pf_audit_refused('api.auth_failed', data: { 'reason' => 'action_not_authorized', 'hook' => hook, 'action' => action.to_s },
210
+ **reference)
211
+ end
212
+
213
+ # `target` is the Permission Set, or the Permission Set Group of a
214
+ # membership; the user ids are bounded (Audit::Refusals).
215
+ def audit_assignment_refused(target, user_ids, hook:)
216
+ pf_audit_refused('api.auth_failed', **PermissionForge::Audit::Refusals.assignment(target, user_ids, hook: hook))
217
+ end
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Api
5
+ class DashboardController < BaseController
6
+ before_action :require_admin!, only: :show
7
+
8
+ def show
9
+ render json: {
10
+ data: {
11
+ permission_sets_count: PermissionForge::PermissionSet.count,
12
+ active_sets_count: PermissionForge::PermissionSet.active.count,
13
+ resources_count: PermissionForge::Resource.count,
14
+ rules_count: PermissionForge::PermissionSetRule.count,
15
+ groups_count: PermissionForge::PermissionSetGroup.count,
16
+ users_count: PermissionForge::PermissionSetUser.select(:user_id).distinct.count
17
+ }
18
+ }, status: :ok
19
+ end
20
+
21
+ private
22
+
23
+ def require_admin!
24
+ return unless PermissionForge.configuration.api_stats_require_admin
25
+
26
+ head :forbidden unless @admin
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Api
5
+ # Members of one Permission Set Group. Adding and removing run through
6
+ # the model callbacks (group_membership.* audit events), after
7
+ # api_assignment_authorization and then config.admin_assignment_authorization.
8
+ class PermissionSetGroupUsersController < BaseController
9
+ include PermissionForge::AssignmentAuthorization
10
+
11
+ before_action :set_group
12
+
13
+ def index
14
+ scope = @group.permission_set_group_users.includes(:user)
15
+ render_collection(scope, serializer_for(:permission_set_group_user))
16
+ end
17
+
18
+ def create
19
+ user_id = params[:user_id]
20
+ authorize_assignment!(@group, [user_id])
21
+ enforce_editable!(@group) unless performed?
22
+ return if performed?
23
+
24
+ user = find_host_user(user_id)
25
+ return if performed? || pf_assignment_refused_api?(@group, user.id, :add_member)
26
+
27
+ gu = @group.permission_set_group_users.build(user: user)
28
+ save_and_render(gu, :permission_set_group_user)
29
+ end
30
+
31
+ # The member must be somebody config.user_list_resolver lists (404 otherwise).
32
+ def destroy
33
+ gu = find_record(@group.permission_set_group_users)
34
+ return unless find_host_user(gu.user_id)
35
+
36
+ authorize_assignment!(@group, [gu.user_id])
37
+ enforce_editable!(gu) unless performed?
38
+ return if performed? || pf_assignment_refused_api?(@group, gu.user_id, :remove_member)
39
+
40
+ gu.destroy!
41
+ head :no_content
42
+ end
43
+
44
+ private
45
+
46
+ def set_group
47
+ @group = find_record(PermissionForge::PermissionSetGroup, :permission_set_group_identifier)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Api
5
+ class PermissionSetGroupsController < BaseController
6
+ before_action :set_group, only: %i[show update destroy clone]
7
+
8
+ def index
9
+ scope = PermissionForge::PermissionSetGroup.order(:name)
10
+ render_collection(scope, serializer_for(:permission_set_group))
11
+ end
12
+
13
+ def show
14
+ render_record(@group, serializer_for(:permission_set_group))
15
+ end
16
+
17
+ def create
18
+ group = PermissionForge::PermissionSetGroup.new(group_params)
19
+ guard_mutation!(:create, group)
20
+ return if performed?
21
+
22
+ if group.save
23
+ render_created(group, serializer_for(:permission_set_group))
24
+ else
25
+ render_validation_errors(group)
26
+ end
27
+ end
28
+
29
+ def update
30
+ guard_mutation!(:update, @group)
31
+ return if performed?
32
+
33
+ if @group.update(group_params)
34
+ render_record(@group, serializer_for(:permission_set_group))
35
+ else
36
+ render_validation_errors(@group)
37
+ end
38
+ end
39
+
40
+ def destroy
41
+ guard_mutation!(:destroy, @group)
42
+ return if performed?
43
+
44
+ @group.destroy!
45
+ head :no_content
46
+ end
47
+
48
+ def clone
49
+ authorize_action!(:clone, @group)
50
+ return if performed?
51
+
52
+ # As the admin's Duplicate: the copy of the group and of its sets is editable.
53
+ cloned = PermissionForge::Services::PermissionSetGroupCloner.call(
54
+ permission_set_group: @group, attrs: { name: pf_text_param(:name).presence || "#{@group.name} (Copy)", is_editable: true },
55
+ unlock: true
56
+ )
57
+ render_created(cloned, serializer_for(:permission_set_group))
58
+ end
59
+
60
+ private
61
+
62
+ def set_group
63
+ @group = find_record(PermissionForge::PermissionSetGroup)
64
+ end
65
+
66
+ def group_params
67
+ pf_require_hash(:permission_set_group).permit(:name, :description, :is_active, :is_editable)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Api
5
+ class PermissionSetRulesController < BaseController
6
+ include CrudActions
7
+ include NestedResourceLookup
8
+
9
+ before_action :set_resource
10
+ before_action :set_rule, only: %i[show update destroy]
11
+
12
+ private
13
+
14
+ def collection_scope = @resource.permission_set_rules
15
+ def current_record = @rule
16
+ def serializer_type = :permission_set_rule
17
+
18
+ def build_record
19
+ record = collection_scope.build(record_params)
20
+ record.permission_set = @permission_set
21
+ record
22
+ end
23
+
24
+ def set_rule
25
+ @rule = find_nested_record(@resource.permission_set_rules)
26
+ end
27
+
28
+ def record_params
29
+ pf_require_hash(:permission_set_rule).permit(
30
+ :name, :field_name, :field_value, :operator,
31
+ :join_type, :field_accessor_path, :include_nulls,
32
+ :is_active, :is_editable, :failure_message
33
+ )
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Api
5
+ # Assignments of one Permission Set. Every grant and revocation runs
6
+ # through the model callbacks (assignment.* audit events), after
7
+ # api_assignment_authorization and then config.admin_assignment_authorization.
8
+ class PermissionSetUsersController < BaseController
9
+ include PermissionForge::AssignmentAuthorization
10
+
11
+ # bulk_action → AssignmentAuthorization action; the keys are the
12
+ # service's VALID_ACTIONS, anything else is a 422 before the service runs.
13
+ BULK_ACTIONS = { 'assign' => :grant, 'revoke' => :revoke }.freeze
14
+
15
+ before_action :set_permission_set
16
+
17
+ def index
18
+ scope = @permission_set.permission_set_users.includes(:user)
19
+ render_collection(scope, serializer_for(:permission_set_user))
20
+ end
21
+
22
+ def create
23
+ authorize_assignment!(@permission_set, [params[:user_id]])
24
+ reject_widening!(:grant) unless performed?
25
+ return if performed?
26
+
27
+ user = find_host_user(params[:user_id])
28
+ return if performed? || pf_assignment_refused_api?(@permission_set, user.id, :grant)
29
+
30
+ create_assignment(user)
31
+ end
32
+
33
+ # The holder must be somebody config.user_list_resolver lists (404
34
+ # otherwise). Revoking narrows access for a granting set, also a locked
35
+ # one; on a locked Muting Permission Set it widens and is refused.
36
+ def destroy
37
+ psu = find_record(@permission_set.permission_set_users)
38
+ return unless find_host_user(psu.user_id)
39
+
40
+ authorize_assignment!(@permission_set, [psu.user_id])
41
+ reject_widening!(:revoke) unless performed?
42
+ return if performed? || pf_assignment_refused_api?(@permission_set, psu.user_id, :revoke)
43
+
44
+ psu.destroy!
45
+ head :no_content
46
+ end
47
+
48
+ def bulk
49
+ authorize_assignment!(@permission_set, params[:user_ids])
50
+ # On a locked set only what narrows access passes (an unknown
51
+ # bulk_action is judged as a grant, then refused as a 422).
52
+ reject_widening!(BULK_ACTIONS.fetch(params[:bulk_action].to_s, :grant)) unless performed?
53
+ return if performed? || bulk_refused?
54
+
55
+ render json: { data: bulk_assignment }, status: :ok
56
+ rescue ArgumentError => e
57
+ render json: { error: { status: 422, messages: e.message } }, status: :unprocessable_entity
58
+ end
59
+
60
+ private
61
+
62
+ # 403 when the change would widen access on a locked set
63
+ # (Admin::EditableLock.widening_lock: which way depends on is_muting).
64
+ def reject_widening!(change)
65
+ reject_locked!(pf_widening_lock(@permission_set, change))
66
+ end
67
+
68
+ def create_assignment(user)
69
+ psu = @permission_set.permission_set_users.build(user: user, expires_at: pf_time_param(:expires_at))
70
+ psu.save ? render_created(psu, serializer_for(:permission_set_user)) : render_validation_errors(psu)
71
+ end
72
+
73
+ # An unknown bulk_action raises ArgumentError (422, rescued in #bulk)
74
+ # before anything is authorized or changed.
75
+ def bulk_refused?
76
+ action = BULK_ACTIONS.fetch(params[:bulk_action].to_s) do |name|
77
+ PermissionForge::Services::BulkPermissionSetAssignment.validate_action!(name)
78
+ raise ArgumentError, "Invalid action: #{name}" # a service action this controller does not map
79
+ end
80
+ pf_assignment_refused_api?(@permission_set, params[:user_ids], action)
81
+ end
82
+
83
+ def bulk_assignment
84
+ PermissionForge::Services::BulkPermissionSetAssignment.call(
85
+ permission_set: @permission_set,
86
+ user_ids: bulk_user_ids,
87
+ action: params[:bulk_action],
88
+ created_by: PermissionForge.current_user,
89
+ expires_at: pf_time_param(:expires_at)
90
+ )
91
+ end
92
+
93
+ # The people of the request within config.user_list_resolver
94
+ # (Admin::PeopleDirectory#find_all); an id naming nobody there is
95
+ # skipped, as an unknown id always was.
96
+ def bulk_user_ids
97
+ PermissionForge::Admin::PeopleDirectory.new.find_all(params[:user_ids]).map(&:id)
98
+ end
99
+
100
+ def set_permission_set
101
+ @permission_set = find_record(PermissionForge::PermissionSet, :permission_set_identifier)
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Api
5
+ class PermissionSetsController < BaseController
6
+ before_action :set_permission_set, only: %i[show update destroy clone]
7
+
8
+ def index
9
+ scope = PermissionForge::PermissionSet.order(created_at: :desc)
10
+ render_collection(scope, serializer_for(:permission_set))
11
+ end
12
+
13
+ def show
14
+ render_record(@permission_set, serializer_for(:permission_set))
15
+ end
16
+
17
+ def create
18
+ ps = PermissionForge::PermissionSet.new(permission_set_params)
19
+ guard_mutation!(:create, ps)
20
+ return if performed?
21
+
22
+ save_set(ps) { render_created(ps, serializer_for(:permission_set)) }
23
+ end
24
+
25
+ # The lock is checked on the stored set, before the request's
26
+ # attributes (is_editable among them) are assigned.
27
+ def update
28
+ guard_mutation!(:update, @permission_set)
29
+ return if performed?
30
+
31
+ @permission_set.assign_attributes(permission_set_params)
32
+ save_set(@permission_set) { render_record(@permission_set, serializer_for(:permission_set)) }
33
+ end
34
+
35
+ def destroy
36
+ guard_mutation!(:destroy, @permission_set)
37
+ return if performed?
38
+
39
+ @permission_set.destroy!
40
+ head :no_content
41
+ end
42
+
43
+ def clone
44
+ authorize_action!(:clone, @permission_set)
45
+ return if performed?
46
+
47
+ # As the admin's Duplicate: an editable copy, outside a locked group.
48
+ cloned = PermissionForge::Admin::PermissionSetFactory.duplicate(
49
+ @permission_set, name: pf_text_param(:name).presence || "#{@permission_set.name} (Copy)"
50
+ )
51
+ render_created(cloned, serializer_for(:permission_set))
52
+ end
53
+
54
+ private
55
+
56
+ # Saves a set whose attributes are assigned, unless it would enter or
57
+ # leave a locked Permission Set Group (as the admin's group page
58
+ # refuses); yields to render on success.
59
+ def save_set(permission_set)
60
+ locked = locked_group_for_move(permission_set)
61
+ return reject_locked!(locked) if locked
62
+
63
+ permission_set.save ? yield : render_validation_errors(permission_set)
64
+ end
65
+
66
+ def locked_group_for_move(permission_set)
67
+ return unless permission_set.permission_set_group_id_changed?
68
+
69
+ PermissionForge::Admin::EditableLock.locked_group_for_move(
70
+ from: permission_set.permission_set_group_id_was, to: permission_set.permission_set_group_id
71
+ )
72
+ end
73
+
74
+ def set_permission_set
75
+ @permission_set = find_record(PermissionForge::PermissionSet)
76
+ end
77
+
78
+ def permission_set_params
79
+ pf_require_hash(:permission_set).permit(
80
+ :name, :description, :is_active, :is_editable, :is_muting,
81
+ :requires_session_flag, :allowed_ips, :allowed_hours_start, :allowed_hours_end,
82
+ :permission_set_group_id
83
+ )
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Api
5
+ class ResourceActionsController < BaseController
6
+ include CrudActions
7
+ include NestedResourceLookup
8
+
9
+ before_action :set_resource
10
+ before_action :set_action, only: %i[show update destroy]
11
+
12
+ private
13
+
14
+ def collection_scope = @resource.resource_actions
15
+ def current_record = @action
16
+ def serializer_type = :resource_action
17
+
18
+ def set_action
19
+ @action = find_nested_record(@resource.resource_actions)
20
+ end
21
+
22
+ def record_params
23
+ pf_require_hash(:resource_action).permit(:name, :label, :description, :is_permitted, :is_active)
24
+ end
25
+
26
+ # A create or a rename must also name a valid action key. Checked after
27
+ # the base checks (authorization, editable lock), so a caller who may
28
+ # not change the record gets its 403 first.
29
+ def guard_mutation!(action, record)
30
+ super
31
+ reject_invalid_key!(record) unless performed? || action == :destroy
32
+ end
33
+
34
+ # Action keys follow ControllerActions::KEY_FORMAT: the admin form, the
35
+ # controller catalog and permission_forge_actions accept nothing else.
36
+ # A row named "Export" or "archive " is not the key of `export` or
37
+ # `archive`: it would read as a restriction in the admin while that
38
+ # controller action stays unmanaged. Checked here rather than in a model
39
+ # validation, so rows that already carry such a name can still be edited
40
+ # and cloned.
41
+ def reject_invalid_key!(record)
42
+ name = requested_name
43
+ return if name.nil? || name.match?(PermissionForge::ControllerActions::KEY_FORMAT)
44
+
45
+ record.errors.add(:name, I18n.t('permission_forge.errors.admin.action_format'))
46
+ render_validation_errors(record)
47
+ end
48
+
49
+ # The name a create or a rename asks for; nil when an update keeps the
50
+ # name the row has.
51
+ def requested_name
52
+ current = current_record&.name
53
+ name = record_params.fetch(:name, current).to_s
54
+ name unless name == current
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Api
5
+ class ResourceFieldsController < BaseController
6
+ include CrudActions
7
+ include NestedResourceLookup
8
+
9
+ before_action :set_resource
10
+ before_action :set_field, only: %i[show update destroy]
11
+
12
+ private
13
+
14
+ def collection_scope = @resource.resource_fields
15
+ def current_record = @field
16
+ def serializer_type = :resource_field
17
+
18
+ def set_field
19
+ @field = find_nested_record(@resource.resource_fields)
20
+ end
21
+
22
+ def record_params
23
+ pf_require_hash(:resource_field).permit(
24
+ :name, :label, :description, :is_active, :is_editable,
25
+ :read_access, :edit_access, :mask_access, :mask_strategy
26
+ )
27
+ end
28
+ end
29
+ end
30
+ end