permission_forge 0.14.1 → 0.15.0

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 (347) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +2372 -0
  3. data/README.md +76 -25
  4. data/app/controllers/concerns/permission_forge/api/crud_actions.rb +3 -2
  5. data/app/controllers/concerns/permission_forge/api/input_shapes.rb +200 -0
  6. data/app/controllers/concerns/permission_forge/api/pagination.rb +12 -5
  7. data/app/controllers/concerns/permission_forge/api/response_renderer.rb +37 -1
  8. data/app/controllers/concerns/permission_forge/api_authentication.rb +13 -2
  9. data/app/controllers/concerns/permission_forge/cache_confirmation.rb +42 -11
  10. data/app/controllers/concerns/permission_forge/change_review.rb +42 -0
  11. data/app/controllers/concerns/permission_forge/copy_refusal.rb +39 -0
  12. data/app/controllers/concerns/permission_forge/param_shapes.rb +207 -7
  13. data/app/controllers/concerns/permission_forge/save_notice.rb +69 -0
  14. data/app/controllers/permission_forge/api/audit_events_controller.rb +1 -1
  15. data/app/controllers/permission_forge/api/base_controller.rb +163 -12
  16. data/app/controllers/permission_forge/api/dashboard_controller.rb +29 -4
  17. data/app/controllers/permission_forge/api/permission_set_group_users_controller.rb +5 -2
  18. data/app/controllers/permission_forge/api/permission_set_groups_controller.rb +5 -4
  19. data/app/controllers/permission_forge/api/permission_set_rules_controller.rb +7 -5
  20. data/app/controllers/permission_forge/api/permission_set_users_controller.rb +80 -19
  21. data/app/controllers/permission_forge/api/permission_sets_controller.rb +12 -6
  22. data/app/controllers/permission_forge/api/resource_actions_controller.rb +1 -1
  23. data/app/controllers/permission_forge/api/resource_fields_controller.rb +3 -4
  24. data/app/controllers/permission_forge/api/resources_controller.rb +26 -12
  25. data/app/controllers/permission_forge/api/unmatched_routes_controller.rb +20 -0
  26. data/app/controllers/permission_forge/application_controller.rb +39 -5
  27. data/app/controllers/permission_forge/assignments_controller.rb +36 -34
  28. data/app/controllers/permission_forge/bulk_assignments_controller.rb +8 -4
  29. data/app/controllers/permission_forge/field_lookups_controller.rb +1 -1
  30. data/app/controllers/permission_forge/maintenance_controller.rb +6 -2
  31. data/app/controllers/permission_forge/people_controller.rb +14 -14
  32. data/app/controllers/permission_forge/permission_set_editor.rb +20 -6
  33. data/app/controllers/permission_forge/permission_set_group_users_controller.rb +3 -1
  34. data/app/controllers/permission_forge/permission_set_groups_controller.rb +23 -9
  35. data/app/controllers/permission_forge/permission_set_rules_controller.rb +20 -10
  36. data/app/controllers/permission_forge/permission_sets_controller.rb +23 -24
  37. data/app/controllers/permission_forge/resource_actions_controller.rb +11 -6
  38. data/app/controllers/permission_forge/resource_fields_controller.rb +37 -19
  39. data/app/controllers/permission_forge/resources_controller.rb +12 -11
  40. data/app/frontend/permission_forge/pf_admin.css +99 -17
  41. data/app/frontend/permission_forge/pf_admin.js +36 -0
  42. data/app/helpers/permission_forge/application_helper.rb +22 -72
  43. data/app/helpers/permission_forge/audit_attribution_helper.rb +77 -0
  44. data/app/helpers/permission_forge/audit_helper.rb +57 -119
  45. data/app/helpers/permission_forge/audit_records_helper.rb +103 -0
  46. data/app/helpers/permission_forge/audit_sentences_helper.rb +56 -2
  47. data/app/helpers/permission_forge/badges_helper.rb +74 -17
  48. data/app/helpers/permission_forge/changes_helper.rb +24 -6
  49. data/app/helpers/permission_forge/conditions_helper.rb +119 -0
  50. data/app/helpers/permission_forge/labels_helper.rb +134 -0
  51. data/app/helpers/permission_forge/people_helper.rb +11 -19
  52. data/app/helpers/permission_forge/rule_problems_helper.rb +73 -0
  53. data/app/helpers/permission_forge/sentences_helper.rb +119 -98
  54. data/app/helpers/permission_forge/settings_helper.rb +2 -2
  55. data/app/helpers/permission_forge/ui_helper.rb +65 -4
  56. data/app/models/permission_forge/audit_event.rb +5 -1
  57. data/app/models/permission_forge/permission_set.rb +208 -18
  58. data/app/models/permission_forge/permission_set_group.rb +10 -4
  59. data/app/models/permission_forge/permission_set_group_user.rb +3 -1
  60. data/app/models/permission_forge/permission_set_rule.rb +254 -17
  61. data/app/models/permission_forge/permission_set_user.rb +38 -3
  62. data/app/models/permission_forge/resource.rb +18 -8
  63. data/app/models/permission_forge/resource_action.rb +15 -6
  64. data/app/models/permission_forge/resource_field.rb +97 -11
  65. data/app/views/permission_forge/assignments/confirm_destroy.html.erb +4 -4
  66. data/app/views/permission_forge/assignments/new.html.erb +10 -6
  67. data/app/views/permission_forge/audit_events/index.html.erb +1 -1
  68. data/app/views/permission_forge/audit_events/show.html.erb +16 -9
  69. data/app/views/permission_forge/bulk_assignments/confirm_revoke.html.erb +3 -3
  70. data/app/views/permission_forge/bulk_assignments/new.html.erb +3 -3
  71. data/app/views/permission_forge/field_lookups/show.html.erb +2 -2
  72. data/app/views/permission_forge/home/_broken_rules_card.html.erb +35 -0
  73. data/app/views/permission_forge/home/_conditions_card.html.erb +28 -0
  74. data/app/views/permission_forge/home/_ending_card.html.erb +11 -5
  75. data/app/views/permission_forge/home/_expired_card.html.erb +1 -1
  76. data/app/views/permission_forge/home/_mask_strategies_card.html.erb +27 -0
  77. data/app/views/permission_forge/home/_privacy_card.html.erb +21 -7
  78. data/app/views/permission_forge/home/_rules_card.html.erb +1 -1
  79. data/app/views/permission_forge/home/_secret_rules_card.html.erb +33 -0
  80. data/app/views/permission_forge/home/_stale_card.html.erb +7 -5
  81. data/app/views/permission_forge/home/_tidy_card.html.erb +1 -1
  82. data/app/views/permission_forge/home/index.html.erb +5 -6
  83. data/app/views/permission_forge/people/_access_tab.html.erb +26 -19
  84. data/app/views/permission_forge/people/_audit_tab.html.erb +8 -8
  85. data/app/views/permission_forge/people/_effective_field_source.html.erb +13 -7
  86. data/app/views/permission_forge/people/_effective_tab.html.erb +16 -15
  87. data/app/views/permission_forge/people/_effective_why.html.erb +20 -16
  88. data/app/views/permission_forge/people/_why_item.html.erb +1 -1
  89. data/app/views/permission_forge/people/confirm_remove_all.html.erb +3 -3
  90. data/app/views/permission_forge/people/erasure_preview.html.erb +1 -1
  91. data/app/views/permission_forge/permission_set_groups/confirm_destroy.html.erb +3 -3
  92. data/app/views/permission_forge/permission_set_groups/index.html.erb +3 -3
  93. data/app/views/permission_forge/permission_set_groups/show.html.erb +28 -19
  94. data/app/views/permission_forge/permission_set_rules/_builder.html.erb +35 -15
  95. data/app/views/permission_forge/permission_set_rules/_preview.html.erb +17 -0
  96. data/app/views/permission_forge/permission_set_rules/confirm_destroy.html.erb +3 -3
  97. data/app/views/permission_forge/permission_set_rules/preview.html.erb +12 -0
  98. data/app/views/permission_forge/permission_sets/_assignments_tab.html.erb +37 -22
  99. data/app/views/permission_forge/permission_sets/_audit_tab.html.erb +4 -4
  100. data/app/views/permission_forge/permission_sets/_condition_problems.html.erb +15 -0
  101. data/app/views/permission_forge/permission_sets/_conditions_tab.html.erb +14 -8
  102. data/app/views/permission_forge/permission_sets/_header.html.erb +6 -5
  103. data/app/views/permission_forge/permission_sets/_records_summary.html.erb +2 -2
  104. data/app/views/permission_forge/permission_sets/_resources_tab.html.erb +4 -4
  105. data/app/views/permission_forge/permission_sets/confirm_destroy.html.erb +4 -4
  106. data/app/views/permission_forge/permission_sets/confirm_switch_mode.html.erb +3 -3
  107. data/app/views/permission_forge/permission_sets/index.html.erb +6 -6
  108. data/app/views/permission_forge/permission_sets/new.html.erb +2 -2
  109. data/app/views/permission_forge/permission_sets/review.html.erb +13 -3
  110. data/app/views/permission_forge/permission_sets/show.html.erb +7 -5
  111. data/app/views/permission_forge/resource_actions/_across_sets.html.erb +7 -10
  112. data/app/views/permission_forge/resource_actions/_effect.html.erb +5 -5
  113. data/app/views/permission_forge/resource_actions/_form.html.erb +1 -1
  114. data/app/views/permission_forge/resource_actions/_managed_note.html.erb +5 -3
  115. data/app/views/permission_forge/resource_actions/confirm_destroy.html.erb +1 -1
  116. data/app/views/permission_forge/resource_actions/new.html.erb +1 -1
  117. data/app/views/permission_forge/resource_actions/show.html.erb +2 -2
  118. data/app/views/permission_forge/resource_fields/_explain.html.erb +19 -15
  119. data/app/views/permission_forge/resource_fields/show.html.erb +21 -12
  120. data/app/views/permission_forge/resources/_actions_tab.html.erb +4 -4
  121. data/app/views/permission_forge/resources/_fields_tab.html.erb +17 -9
  122. data/app/views/permission_forge/resources/_rules_tab.html.erb +9 -6
  123. data/app/views/permission_forge/resources/_sync.html.erb +8 -5
  124. data/app/views/permission_forge/resources/confirm_destroy.html.erb +3 -3
  125. data/app/views/permission_forge/resources/new.html.erb +3 -2
  126. data/app/views/permission_forge/resources/show.html.erb +6 -6
  127. data/app/views/permission_forge/settings/_row.html.erb +2 -2
  128. data/app/views/permission_forge/settings/show.html.erb +10 -1
  129. data/app/views/permission_forge/shared/_across_sets.html.erb +8 -9
  130. data/app/views/permission_forge/shared/_audit_events.html.erb +2 -2
  131. data/app/views/permission_forge/shared/_audit_footer.html.erb +13 -2
  132. data/app/views/permission_forge/shared/_crud_cell.html.erb +6 -6
  133. data/app/views/permission_forge/shared/_mode_banners.html.erb +1 -1
  134. data/app/views/permission_forge/shared/_savebar.html.erb +4 -2
  135. data/config/locales/permission_forge.en.yml +480 -69
  136. data/config/locales/permission_forge.mk.yml +499 -79
  137. data/config/locales/permission_forge.sq.yml +509 -80
  138. data/config/routes.rb +38 -23
  139. data/lib/generators/permission_forge/install/install_generator.rb +21 -0
  140. data/lib/generators/permission_forge/install/templates/initializer.rb.tt +35 -34
  141. data/lib/generators/permission_forge/install/templates/migration.rb.tt +5 -5
  142. data/lib/permission_forge/activation_conditions.rb +200 -0
  143. data/lib/permission_forge/admin/action_label.rb +33 -0
  144. data/lib/permission_forge/admin/action_rows.rb +3 -1
  145. data/lib/permission_forge/admin/actor_label.rb +52 -0
  146. data/lib/permission_forge/admin/attention_items.rb +152 -31
  147. data/lib/permission_forge/admin/audit_log.rb +4 -7
  148. data/lib/permission_forge/admin/audit_trail.rb +33 -17
  149. data/lib/permission_forge/admin/bulk_people_matcher.rb +32 -14
  150. data/lib/permission_forge/admin/chain_entries.rb +42 -0
  151. data/lib/permission_forge/admin/change_set.rb +12 -1
  152. data/lib/permission_forge/admin/change_set_parts/base.rb +5 -4
  153. data/lib/permission_forge/admin/change_set_parts/context.rb +3 -1
  154. data/lib/permission_forge/admin/change_set_parts/field_edit.rb +4 -1
  155. data/lib/permission_forge/admin/change_set_parts/field_levels.rb +1 -1
  156. data/lib/permission_forge/admin/change_set_parts/new_actions.rb +1 -1
  157. data/lib/permission_forge/admin/change_set_parts/new_fields.rb +38 -11
  158. data/lib/permission_forge/admin/change_set_parts/resource_edit.rb +17 -1
  159. data/lib/permission_forge/admin/change_set_parts/resource_flags.rb +1 -1
  160. data/lib/permission_forge/admin/change_set_parts/set_attrs.rb +4 -0
  161. data/lib/permission_forge/admin/change_set_parts/values.rb +10 -4
  162. data/lib/permission_forge/admin/column_fields.rb +22 -21
  163. data/lib/permission_forge/admin/conditions_form.rb +47 -15
  164. data/lib/permission_forge/admin/controller_catalog.rb +42 -14
  165. data/lib/permission_forge/admin/editable_lock.rb +83 -3
  166. data/lib/permission_forge/admin/effective_access.rb +147 -48
  167. data/lib/permission_forge/admin/effective_fields.rb +75 -67
  168. data/lib/permission_forge/admin/error_sentence.rb +47 -0
  169. data/lib/permission_forge/admin/expiry_presets.rb +17 -0
  170. data/lib/permission_forge/admin/exports/holdings.rb +6 -3
  171. data/lib/permission_forge/admin/field_across_sets.rb +7 -4
  172. data/lib/permission_forge/admin/field_chain.rb +109 -0
  173. data/lib/permission_forge/admin/field_detail.rb +24 -11
  174. data/lib/permission_forge/admin/field_label.rb +109 -0
  175. data/lib/permission_forge/admin/field_level.rb +4 -68
  176. data/lib/permission_forge/admin/field_provenance.rb +132 -0
  177. data/lib/permission_forge/admin/field_rows.rb +26 -11
  178. data/lib/permission_forge/admin/last_changes.rb +14 -0
  179. data/lib/permission_forge/admin/list_sentence.rb +32 -0
  180. data/lib/permission_forge/admin/mask_strategy_choices.rb +57 -0
  181. data/lib/permission_forge/admin/model_catalog.rb +83 -39
  182. data/lib/permission_forge/admin/model_label.rb +66 -0
  183. data/lib/permission_forge/admin/people_candidates.rb +21 -9
  184. data/lib/permission_forge/admin/people_directory.rb +23 -5
  185. data/lib/permission_forge/admin/permission_set_factory.rb +6 -15
  186. data/lib/permission_forge/admin/person_label.rb +105 -16
  187. data/lib/permission_forge/admin/prefill.rb +92 -0
  188. data/lib/permission_forge/admin/record_name.rb +45 -0
  189. data/lib/permission_forge/admin/rule_builder.rb +77 -3
  190. data/lib/permission_forge/admin/rule_form.rb +44 -10
  191. data/lib/permission_forge/admin/rule_form_input.rb +27 -11
  192. data/lib/permission_forge/admin/rule_notes.rb +43 -0
  193. data/lib/permission_forge/admin/rule_preview.rb +96 -0
  194. data/lib/permission_forge/admin/sensitive_columns.rb +10 -62
  195. data/lib/permission_forge/admin/settings_catalog.rb +64 -12
  196. data/lib/permission_forge/admin/state_label.rb +97 -0
  197. data/lib/permission_forge/admin_method_override.rb +44 -0
  198. data/lib/permission_forge/api/schema.rb +322 -0
  199. data/lib/permission_forge/api/serializers/base_serializer.rb +87 -1
  200. data/lib/permission_forge/api/serializers/permission_set_group_user_serializer.rb +1 -1
  201. data/lib/permission_forge/api/serializers/permission_set_rule_serializer.rb +1 -1
  202. data/lib/permission_forge/api/serializers/permission_set_serializer.rb +5 -2
  203. data/lib/permission_forge/api/serializers/permission_set_user_serializer.rb +1 -1
  204. data/lib/permission_forge/api/serializers/resource_action_serializer.rb +1 -1
  205. data/lib/permission_forge/api/serializers/resource_field_serializer.rb +1 -1
  206. data/lib/permission_forge/api/serializers/resource_serializer.rb +1 -1
  207. data/lib/permission_forge/api_authorizations.rb +59 -0
  208. data/lib/permission_forge/api_only_admin.rb +81 -0
  209. data/lib/permission_forge/audit/actor_codes.rb +32 -0
  210. data/lib/permission_forge/audit/actor_resolver.rb +1 -2
  211. data/lib/permission_forge/audit/boot_warnings.rb +29 -9
  212. data/lib/permission_forge/audit/event_catalog.rb +16 -6
  213. data/lib/permission_forge/audit/event_query.rb +3 -3
  214. data/lib/permission_forge/audit/reason_codes.rb +25 -0
  215. data/lib/permission_forge/audit/runtime.rb +9 -2
  216. data/lib/permission_forge/audit/settings_observer.rb +2 -2
  217. data/lib/permission_forge/audit/unmasked_without_user.rb +169 -0
  218. data/lib/permission_forge/audit.rb +24 -10
  219. data/lib/permission_forge/boot_checks.rb +180 -63
  220. data/lib/permission_forge/cache/base.rb +10 -0
  221. data/lib/permission_forge/cache/null_cache.rb +5 -0
  222. data/lib/permission_forge/cache/outcome.rb +119 -0
  223. data/lib/permission_forge/cache/rails_cache.rb +6 -0
  224. data/lib/permission_forge/cache/report.rb +150 -0
  225. data/lib/permission_forge/cache/user_keys.rb +8 -4
  226. data/lib/permission_forge/cache_warnings.rb +140 -0
  227. data/lib/permission_forge/concerns/auto_permission_scoping.rb +19 -2
  228. data/lib/permission_forge/concerns/has_tenant_scoping.rb +1 -1
  229. data/lib/permission_forge/concerns/has_uuid.rb +1 -1
  230. data/lib/permission_forge/concerns/keeps_copied_values.rb +48 -0
  231. data/lib/permission_forge/concerns/masked_attributes/alias_access.rb +12 -2
  232. data/lib/permission_forge/concerns/masked_attributes/virtual_readers.rb +6 -4
  233. data/lib/permission_forge/concerns/masked_attributes.rb +150 -34
  234. data/lib/permission_forge/concerns/normalizes_label.rb +78 -0
  235. data/lib/permission_forge/concerns/permission_cache_accessor.rb +3 -3
  236. data/lib/permission_forge/concerns/permission_scoped_findable.rb +121 -29
  237. data/lib/permission_forge/concerns/permissioned_user.rb +1 -1
  238. data/lib/permission_forge/concerns/records_audit_events.rb +13 -6
  239. data/lib/permission_forge/concerns/requires_name.rb +37 -0
  240. data/lib/permission_forge/concerns/validates_column_lengths.rb +24 -0
  241. data/lib/permission_forge/concerns/validates_dependency.rb +5 -2
  242. data/lib/permission_forge/concerns/validates_required_columns.rb +74 -0
  243. data/lib/permission_forge/conditional_sets.rb +13 -6
  244. data/lib/permission_forge/configuration/admin_ui_settings.rb +18 -4
  245. data/lib/permission_forge/configuration/audit_settings.rb +2 -1
  246. data/lib/permission_forge/configuration/controller_action_settings.rb +5 -4
  247. data/lib/permission_forge/configuration/masked_attributes_settings.rb +73 -19
  248. data/lib/permission_forge/configuration/presets.rb +20 -11
  249. data/lib/permission_forge/configuration/query_scoping_settings.rb +55 -0
  250. data/lib/permission_forge/configuration/strategy_values.rb +2 -1
  251. data/lib/permission_forge/configuration.rb +41 -18
  252. data/lib/permission_forge/controller_actions/managed_index.rb +8 -7
  253. data/lib/permission_forge/database_reach.rb +93 -0
  254. data/lib/permission_forge/deprecation.rb +1 -1
  255. data/lib/permission_forge/docs/api_samples.rb +250 -0
  256. data/lib/permission_forge/docs/configuration_reference.rb +172 -0
  257. data/lib/permission_forge/encrypted_attributes.rb +150 -0
  258. data/lib/permission_forge/engine.rb +46 -14
  259. data/lib/permission_forge/execution_state/restore.rb +74 -0
  260. data/lib/permission_forge/execution_state.rb +8 -1
  261. data/lib/permission_forge/field_level.rb +123 -0
  262. data/lib/permission_forge/field_names.rb +145 -0
  263. data/lib/permission_forge/field_variants.rb +96 -0
  264. data/lib/permission_forge/generated_text.rb +95 -0
  265. data/lib/permission_forge/inspection_filters.rb +83 -0
  266. data/lib/permission_forge/managed_fields.rb +66 -0
  267. data/lib/permission_forge/masked_models.rb +25 -19
  268. data/lib/permission_forge/masking_sync.rb +100 -25
  269. data/lib/permission_forge/model_bindings.rb +30 -19
  270. data/lib/permission_forge/model_lookup.rb +42 -0
  271. data/lib/permission_forge/operators/encrypted_alias.rb +67 -0
  272. data/lib/permission_forge/operators/registry.rb +6 -0
  273. data/lib/permission_forge/operators/rule_column.rb +242 -27
  274. data/lib/permission_forge/operators/rule_path.rb +179 -0
  275. data/lib/permission_forge/operators/rule_problem.rb +309 -0
  276. data/lib/permission_forge/operators/rule_sources.rb +96 -0
  277. data/lib/permission_forge/operators/value_casters/postgresql_types.rb +237 -0
  278. data/lib/permission_forge/operators/value_casters/temporal.rb +9 -20
  279. data/lib/permission_forge/operators/value_casters.rb +61 -13
  280. data/lib/permission_forge/operators/value_typing.rb +83 -7
  281. data/lib/permission_forge/query_scoping_warnings.rb +22 -17
  282. data/lib/permission_forge/rake_audit.rb +5 -4
  283. data/lib/permission_forge/rake_dry_run.rb +32 -0
  284. data/lib/permission_forge/rake_output.rb +189 -0
  285. data/lib/permission_forge/record_read_access.rb +51 -0
  286. data/lib/permission_forge/resource_chain.rb +14 -2
  287. data/lib/permission_forge/schema_read/ordered_types.rb +72 -0
  288. data/lib/permission_forge/schema_read.rb +335 -0
  289. data/lib/permission_forge/secret_columns.rb +49 -0
  290. data/lib/permission_forge/sensitive_columns.rb +114 -0
  291. data/lib/permission_forge/services/attribute_access_guard.rb +22 -14
  292. data/lib/permission_forge/services/bulk_permission_set_assignment.rb +16 -8
  293. data/lib/permission_forge/services/concerns/clonable.rb +9 -0
  294. data/lib/permission_forge/services/concerns/settles_cache.rb +58 -0
  295. data/lib/permission_forge/services/consent_withdrawal_handler.rb +37 -19
  296. data/lib/permission_forge/services/data_subject_erasure.rb +83 -45
  297. data/lib/permission_forge/services/expired_assignment_cleaner.rb +62 -25
  298. data/lib/permission_forge/services/extra_field_defaults.rb +1 -1
  299. data/lib/permission_forge/services/field_permission_merger.rb +44 -17
  300. data/lib/permission_forge/services/filters_response_attributes.rb +20 -5
  301. data/lib/permission_forge/services/gdpr_field_auditor/text_report.rb +33 -5
  302. data/lib/permission_forge/services/gdpr_field_auditor.rb +68 -48
  303. data/lib/permission_forge/services/host_key_guard.rb +8 -5
  304. data/lib/permission_forge/services/mask_value.rb +72 -11
  305. data/lib/permission_forge/services/permission_cache_clearer.rb +36 -15
  306. data/lib/permission_forge/services/permission_hash_builder.rb +48 -19
  307. data/lib/permission_forge/services/permission_rule_applier.rb +131 -68
  308. data/lib/permission_forge/services/permission_set_cloner.rb +32 -8
  309. data/lib/permission_forge/services/permission_set_condition_evaluator.rb +38 -36
  310. data/lib/permission_forge/services/permission_set_group_cloner.rb +2 -1
  311. data/lib/permission_forge/services/permits_attributes/result.rb +31 -0
  312. data/lib/permission_forge/services/permits_attributes.rb +48 -27
  313. data/lib/permission_forge/services/resource_field_generator/row_builder.rb +96 -0
  314. data/lib/permission_forge/services/resource_field_generator.rb +71 -42
  315. data/lib/permission_forge/services/resource_with_fields_builder.rb +2 -1
  316. data/lib/permission_forge/services/rls_operator_sql.rb +83 -24
  317. data/lib/permission_forge/services/rls_policy_generator.rb +83 -39
  318. data/lib/permission_forge/services/rls_read_access_sql.rb +131 -0
  319. data/lib/permission_forge/services/security_audit/action_check.rb +49 -5
  320. data/lib/permission_forge/services/security_audit/assignment_check.rb +2 -1
  321. data/lib/permission_forge/services/security_audit/audit_trail_check.rb +1 -3
  322. data/lib/permission_forge/services/security_audit/conditions_check.rb +92 -0
  323. data/lib/permission_forge/services/security_audit/config_check.rb +41 -18
  324. data/lib/permission_forge/services/security_audit/integrity_check.rb +71 -4
  325. data/lib/permission_forge/services/security_audit/masking_without_user.rb +66 -0
  326. data/lib/permission_forge/services/security_audit/personal_data_check.rb +7 -11
  327. data/lib/permission_forge/services/security_audit/rule_check.rb +180 -50
  328. data/lib/permission_forge/services/security_audit/text_report.rb +14 -2
  329. data/lib/permission_forge/services/security_audit/unmanaged_actions.rb +145 -0
  330. data/lib/permission_forge/services/security_audit/user_model_check.rb +4 -3
  331. data/lib/permission_forge/services/security_audit.rb +16 -4
  332. data/lib/permission_forge/services/stale_assignment_auditor.rb +8 -5
  333. data/lib/permission_forge/timestamp_fields.rb +60 -0
  334. data/lib/permission_forge/unique_conflict.rb +117 -0
  335. data/lib/permission_forge/version.rb +1 -1
  336. data/lib/permission_forge/virtual_fields/discovery.rb +2 -1
  337. data/lib/permission_forge/virtual_fields.rb +15 -5
  338. data/lib/permission_forge/warnings.rb +263 -0
  339. data/lib/permission_forge/web_request.rb +248 -0
  340. data/lib/permission_forge.rb +54 -0
  341. data/lib/tasks/permission_forge.rake +26 -6
  342. data/lib/tasks/permission_forge_audit_events.rake +8 -4
  343. data/lib/tasks/permission_forge_cache.rake +12 -12
  344. data/lib/tasks/permission_forge_docs.rake +48 -0
  345. data/lib/tasks/permission_forge_expiration.rake +11 -5
  346. data/lib/tasks/permission_forge_gdpr.rake +36 -20
  347. metadata +84 -1
data/config/routes.rb CHANGED
@@ -85,6 +85,7 @@ PermissionForge::Engine.routes.draw do
85
85
  resources :rules, controller: 'permission_set_rules', only: %i[new create edit update destroy] do
86
86
  collection do
87
87
  post :refresh
88
+ get :preview
88
89
  end
89
90
  member do
90
91
  patch :toggle
@@ -107,37 +108,51 @@ PermissionForge::Engine.routes.draw do
107
108
  # JSON Admin API — routes are always drawn; access is guarded at runtime
108
109
  # by BaseController checking api_enabled + api_auth configuration.
109
110
  namespace :api, defaults: { format: :json } do
110
- resource :stats, only: [:show], controller: 'dashboard'
111
-
112
- # The audit log is read-only over the API: no write route exists.
113
- resources :audit_events, only: %i[index show], param: :identifier
111
+ # JSON only: `.json` or no extension. Any other extension
112
+ # (`/permission_sets.xml`) matches no endpoint and reaches the catch-all
113
+ # below: the JSON 404 envelope, never a JSON 200 under another format.
114
+ constraints(format: /json/) do
115
+ resource :stats, only: [:show], controller: 'dashboard'
116
+
117
+ # The audit log is read-only over the API: no write route exists.
118
+ resources :audit_events, only: %i[index show], param: :identifier
119
+
120
+ # No new/edit: a JSON API has no forms. Without them `…/new` reads as
121
+ # an identifier (404 unknown record) and `…/:identifier/edit` reaches
122
+ # the catch-all (404), both in the envelope; through 0.14.1 they raised
123
+ # AbstractController::ActionNotFound (Rails' own 404 page).
124
+ resources :permission_sets, param: :identifier, except: %i[new edit] do
125
+ member do
126
+ post :clone
127
+ end
114
128
 
115
- resources :permission_sets, param: :identifier do
116
- member do
117
- post :clone
118
- end
129
+ resources :resources, param: :identifier, except: %i[new edit] do
130
+ member do
131
+ post :generate_fields
132
+ end
133
+ resources :fields, param: :identifier, controller: 'resource_fields', except: %i[new edit]
134
+ resources :actions, param: :identifier, controller: 'resource_actions', except: %i[new edit]
135
+ resources :rules, param: :identifier, controller: 'permission_set_rules', except: %i[new edit]
136
+ end
119
137
 
120
- resources :resources, param: :identifier do
121
- member do
122
- post :generate_fields
138
+ resources :users, param: :identifier, controller: 'permission_set_users', only: %i[index create destroy] do
139
+ collection do
140
+ post :bulk
141
+ end
123
142
  end
124
- resources :fields, param: :identifier, controller: 'resource_fields'
125
- resources :actions, param: :identifier, controller: 'resource_actions'
126
- resources :rules, param: :identifier, controller: 'permission_set_rules'
127
143
  end
128
144
 
129
- resources :users, param: :identifier, controller: 'permission_set_users', only: %i[index create destroy] do
130
- collection do
131
- post :bulk
145
+ resources :permission_set_groups, param: :identifier, except: %i[new edit] do
146
+ member do
147
+ post :clone
132
148
  end
149
+ resources :users, param: :identifier, controller: 'permission_set_group_users', only: %i[index create destroy]
133
150
  end
134
151
  end
135
152
 
136
- resources :permission_set_groups, param: :identifier do
137
- member do
138
- post :clone
139
- end
140
- resources :users, param: :identifier, controller: 'permission_set_group_users', only: %i[index create destroy]
141
- end
153
+ # Last: any other path or method under /api answers the JSON 404
154
+ # envelope, not Rails' HTML page (Api::UnmatchedRoutesController) —
155
+ # OPTIONS and TRACE included (through 0.15.0-rc a RoutingError).
156
+ match '(*path)', to: 'unmatched_routes#show', via: %i[get post put patch delete options trace], format: false
142
157
  end
143
158
  end
@@ -23,6 +23,16 @@ module PermissionForge
23
23
 
24
24
  KEY_TYPES = %w[bigint integer uuid string].freeze
25
25
  DETECTED_KEY_TYPES = { integer: 'bigint', uuid: 'uuid', string: 'string' }.freeze
26
+ # Printed after an install (#print_next_steps).
27
+ NEXT_STEPS = [
28
+ 'bin/rails db:migrate',
29
+ 'In your User model: include PermissionForge::Concerns::PermissionedUser (can_read?, permissions_hash)',
30
+ 'Mount the admin in config/routes.rb: mount PermissionForge::Engine, at: "/permissions"',
31
+ "Admin credentials: uncomment Option A in config/initializers/permission_forge.rb\n " \
32
+ '(PF_ADMIN_USER, PF_ADMIN_PASSWORD), or use your own admin controller (Option B)',
33
+ 'bin/rails permission_forge:audit lists what is still open before production'
34
+ ].freeze
35
+ QUICK_START_URL = 'https://github.com/fetijashari/permission_forge#quick-start'.freeze
26
36
 
27
37
  def copy_initializer
28
38
  template 'initializer.rb.tt', 'config/initializers/permission_forge.rb'
@@ -34,6 +44,17 @@ module PermissionForge
34
44
  migration_template 'migration.rb.tt', 'db/migrate/create_permission_forge_tables.rb'
35
45
  end
36
46
 
47
+ # What the files alone do not do: without a mount and credentials there
48
+ # is no admin to open. Not printed by `rails destroy`.
49
+ def print_next_steps
50
+ return unless behavior == :invoke
51
+
52
+ steps = NEXT_STEPS.drop(options[:skip_migration] ? 1 : 0)
53
+ say "\nPermissionForge is installed. Next steps:"
54
+ steps.each.with_index(1) { |step, number| say " #{number}. #{step}" }
55
+ say " Quick Start: #{QUICK_START_URL}"
56
+ end
57
+
37
58
  private
38
59
 
39
60
  def table_prefix
@@ -10,23 +10,23 @@ PermissionForge.configure do |config|
10
10
  # ---------------------------------------------------------------------------
11
11
  # A guarded controller action (ControllerActionGuard) with nobody signed in is refused.
12
12
  config.action_guard_nil_user_strategy = :deny
13
+ # So, by default, is any guarded action no Permission Set manages (opt out: config.unconfigured_action_strategy = :allow).
13
14
  # Conditional Permission Sets fail closed without a request context (see below).
14
15
  config.conditional_sets_strict_mode = true
15
- # The cache is Rails.cache: use a shared store in production (Redis, Solid Cache, Memcached).
16
+ # Rails.cache, shared by every process: Redis or Solid Cache in production, :file_store (not :memory_store) in development.
16
17
  config.cache_backend = :rails_cache
17
18
 
18
- # config.preset = :strict closes every fail-open default at once
19
- # (docs/configuration.md#presets; a setting you make here still wins):
20
- # nil_user_strategy = :deny, auto_query_scoping = :filter_only,
21
- # response_filter_nil_user_strategy = :block, unconfigured_action_strategy = :block,
22
- # mask_virtual_attributes = :auto, extra_fields_default_access = :unconfigured,
23
- # and the three settings above.
19
+ # config.preset = :strict closes the fail-open defaults a setting can close (masking with
20
+ # nobody signed in too: :hide_in_requests, set in its own section below):
21
+ # nil_user_strategy = :deny, auto_query_scoping = :filter_only, no_read_access_strategy = :deny,
22
+ # response_filter_nil_user_strategy = :block, mask_virtual_attributes = :auto,
23
+ # extra_fields_default_access = :unconfigured, and the three settings above. A setting you make here still wins (docs/configuration.md#presets).
24
24
  # What it costs: with nil_user_strategy :deny and auto_query_scoping :filter_only,
25
- # code that runs with no current user (jobs, mailers, seeds, webhooks, the
26
- # console, sign-in lookups on a user model that includes PermissionScopedFindable)
27
- # sees no protected rows, and a belongs_to presence check on a protected parent
28
- # fails ("must exist"). Run such code with a user, or inside
29
- # PermissionForge.without_permission_scope { }, where nobody signed in gets no rules:
25
+ # code that runs with no current user (jobs, mailers, seeds, webhooks, the console,
26
+ # sign-in lookups on a user model that includes PermissionScopedFindable) sees no
27
+ # protected rows (a seed's find_or_create_by! adds a duplicate on every run), and a
28
+ # belongs_to presence check on a protected parent fails ("must exist"). Run such code
29
+ # with a user, or inside PermissionForge.without_permission_scope { }, where nobody signed in gets no rules:
30
30
  # never around code an anonymous request reaches. Never include PermissionScopedFindable in the user model.
31
31
  # config.preset = :strict
32
32
 
@@ -48,10 +48,9 @@ PermissionForge.configure do |config|
48
48
  # reports the assignments still held by users it calls inactive.
49
49
  config.user_active_check = ->(user) { user.respond_to?(:active_for_authentication?) ? user.active_for_authentication? : true }
50
50
 
51
- # The profile cached with each user's permissions (uuid: nil when users have none).
52
- config.profile_builder = ->(user) {
53
- { id: user.id, uuid: user.try(:uuid), name: user.try(:name), email: user.try(:email) }
54
- }
51
+ # The profile cached with each user's permissions. The default is like this example but leaves an encrypted
52
+ # attribute out (nil); this lambda calls the public readers, which decrypt (and may be masked):
53
+ # config.profile_builder = ->(user) { { id: user.id, uuid: user.try(:uuid), name: user.try(:name), email: user.try(:email).presence || user.try(:email_address) } }
55
54
 
56
55
  # Prefix of the PermissionForge tables ('' for none).
57
56
  config.table_name_prefix = '<%= table_prefix %>'
@@ -85,14 +84,15 @@ PermissionForge.configure do |config|
85
84
  # keep it cheap, never read permissions inside it, return {} with no request.
86
85
 
87
86
  # ---------------------------------------------------------------------------
88
- # Masked attributes with nobody signed in (not part of the preset)
87
+ # Masked attributes with nobody signed in
89
88
  # ---------------------------------------------------------------------------
90
- # :bypass (default) returns the stored values; :hide hides every guarded
91
- # attribute. Trade-off: jobs, mailers and console code that read masked
92
- # models with no user then see hidden values: set the current user there.
93
- # The user model (user_class and its subclasses) stays readable for sign-in,
94
- # and validations always read the stored values.
95
- # config.masked_attributes_nil_user_strategy = :hide
89
+ # Hidden in a web request with nobody signed in (a public page, a forgotten
90
+ # sign-in, a before_action before it, a mail sent with deliver_now); jobs, rake
91
+ # and the console keep stored values. :hide hides in jobs too; :bypass nowhere.
92
+ # Webhooks that normalize records: sign a system user in (a value derived from
93
+ # a hidden read can still be saved into another column).
94
+ # The user model stays readable for sign-in; validations read stored values.
95
+ config.masked_attributes_nil_user_strategy = :hide_in_requests
96
96
 
97
97
  # ---------------------------------------------------------------------------
98
98
  # Admin screens
@@ -108,15 +108,15 @@ PermissionForge.configure do |config|
108
108
  # config.http_basic_auth_enabled = false
109
109
  # config.base_controller_class = '::Admin::BaseController' # must authenticate admins
110
110
  #
111
- # Who may grant, revoke, extend or review an assignment, or change a group's
112
- # members (admin screens and API). Example: no self-assignment.
111
+ # Who may grant, revoke, extend or review an assignment, or change a group's members
112
+ # (admin screens and API). Example: no self-assignment. PermissionForge.current_user is
113
+ # current_user_resolver's user; under Option A your app's sign-in does not run (nil).
113
114
  # config.admin_assignment_authorization = ->(controller, target, user_ids, action) {
114
- # admin = controller.current_user
115
+ # admin = PermissionForge.current_user
115
116
  # admin&.admin? && !user_ids.include?(admin.id)
116
117
  # }
117
- #
118
118
  # Who may read the audit log pages, exports and API (nil: anyone with admin access).
119
- # config.audit_log_authorization = ->(controller) { controller.current_user&.auditor? }
119
+ # config.audit_log_authorization = ->(controller) { PermissionForge.current_user&.auditor? }
120
120
 
121
121
  # ---------------------------------------------------------------------------
122
122
  # Audit log (docs/audit-log.md)
@@ -124,7 +124,8 @@ PermissionForge.configure do |config|
124
124
  # Every permission change is recorded in the audit_events table, inside the
125
125
  # change's own transaction. Each option with its default (an example where that is nil):
126
126
  # config.audit_events = :auto # true: always; false: never (events are still published)
127
- # config.audit_actor_resolver = ->(controller) { controller.current_user } # nil: current_user_resolver
127
+ # config.audit_actor_resolver = ->(controller) { Session.find_by(id: controller.request.cookie_jar.signed[:session_id])&.user }
128
+ # nil: current_user_resolver, then the Basic auth login; the example names a Rails 8 sign-in under Option A
128
129
  # config.audit_request_context = ->(controller) { { request_id: controller.request.request_id, ip: controller.request.remote_ip } }
129
130
  # config.audit_change_failure_strategy = :raise # the change rolls back; or :report, :log
130
131
  # config.audit_hook_failure_strategy = :report # or :log, :raise
@@ -138,11 +139,11 @@ PermissionForge.configure do |config|
138
139
  # ---------------------------------------------------------------------------
139
140
  # config.api_enabled = true
140
141
  # config.api_base_controller_class = '::Api::BaseController' # your auth chain
141
- # config.api_auth = ->(controller) { controller.current_user&.admin? }
142
- # config.api_action_authorization = ->(controller, action, record) { controller.current_user&.admin? }
143
- # config.api_assignment_authorization = ->(controller, permission_set, user_ids) { controller.current_user&.admin? }
142
+ # config.api_auth = ->(controller) { PermissionForge.current_user&.admin? }
143
+ # config.api_action_authorization = ->(controller, action, record) { PermissionForge.current_user&.admin? }
144
+ # config.api_assignment_authorization = ->(controller, permission_set, user_ids) { PermissionForge.current_user&.admin? }
144
145
  #
145
- # api_auth proves identity only: until both authorization lambdas are set,
146
- # every API change and assignment is refused (403).
146
+ # config.api_stats_authorization = ->(controller) { PermissionForge.current_user&.admin? } # GET /api/stats
147
+ # api_auth proves identity only: until each lambda above is set, what it guards is refused (403).
147
148
  # config.api_require_authorization = true # default
148
149
  end
@@ -2,7 +2,7 @@ class CreatePermissionForgeTables < ActiveRecord::Migration<%= migration_version
2
2
  def change
3
3
  create_table :<%= table_prefix %>permission_set_groups do |t|
4
4
  t.string :uuid, null: false
5
- t.string :name, null: false, default: 'Untitled Permission Set Group'
5
+ t.string :name, null: false
6
6
  t.string :description
7
7
  t.boolean :is_active, null: false, default: true
8
8
  t.boolean :is_editable, null: false, default: true
@@ -18,7 +18,7 @@ class CreatePermissionForgeTables < ActiveRecord::Migration<%= migration_version
18
18
  create_table :<%= table_prefix %>permission_sets do |t|
19
19
  t.string :uuid, null: false
20
20
  t.references :permission_set_group, foreign_key: { to_table: :<%= table_prefix %>permission_set_groups }, null: true
21
- t.string :name, null: false, default: 'Untitled Permission Set'
21
+ t.string :name, null: false
22
22
  t.string :description
23
23
  t.boolean :is_active, null: false, default: true
24
24
  t.boolean :is_editable, null: false, default: true
@@ -106,14 +106,14 @@ class CreatePermissionForgeTables < ActiveRecord::Migration<%= migration_version
106
106
  t.string :uuid, null: false
107
107
  t.references :permission_set, foreign_key: { to_table: :<%= table_prefix %>permission_sets }, null: false
108
108
  t.references :resource, foreign_key: { to_table: :<%= table_prefix %>resources }, null: false
109
- t.string :name, null: false, default: 'Untitled Rule'
110
- t.string :failure_message, default: 'You do not have permission to perform this action.'
109
+ t.string :name, null: false
110
+ t.string :failure_message
111
111
  t.string :join_type
112
112
  t.string :field_name, null: false
113
113
  t.string :field_value, null: false
114
114
  t.string :field_accessor_path
115
115
  t.string :operator, null: false, default: 'equal'
116
- t.boolean :include_nulls, null: false, default: true
116
+ t.boolean :include_nulls, null: false, default: false
117
117
  t.boolean :is_active, null: false, default: true
118
118
  t.boolean :is_editable, null: false, default: true
119
119
  <% if tenant_column %> t.<%= tenant_id_type %> :<%= tenant_column %>
@@ -0,0 +1,200 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ipaddr'
4
+
5
+ module PermissionForge
6
+ # The activation conditions of a Permission Set as its row holds them: the
7
+ # one reading of `allowed_ips` (a list of IP addresses or CIDR ranges
8
+ # written with commas, newlines or spaces between them) and of the hour
9
+ # window (`allowed_hours_start`, `allowed_hours_end`). Included in
10
+ # PermissionSet (the row) and in ConditionalSets::Member (what the cache
11
+ # keeps of a conditional set); anything else that answers the three
12
+ # readers is read through {.of}.
13
+ #
14
+ # Who reads it: Services::PermissionSetConditionEvaluator (every
15
+ # per-request selection), the PermissionSet validations (a new row, or a
16
+ # changed condition, is refused when it cannot be judged — the stored text
17
+ # is normalised to commas), Admin::ConditionsForm, SentencesHelper, the
18
+ # API serializer and the `conditions` security-audit check.
19
+ #
20
+ # A condition nobody can judge — an entry that is not an address, a list
21
+ # of separators alone, one hour of the two, the same hour twice, an hour
22
+ # outside 0..23 — is *broken* (#conditions_broken?): a granting set never
23
+ # passes on it, with or without a request context, and a muting set always
24
+ # applies (ConditionalSets::Member), in every mode. The condition can only be written past the model.
25
+ #
26
+ # @api private
27
+ module ActivationConditions
28
+ # What separates two entries of an IP list.
29
+ SEPARATORS = /[,\s]+/
30
+ # The hours of a day.
31
+ HOURS = (0..23)
32
+
33
+ # The hour window of a set: none (#set? false), a window from `from`
34
+ # (inclusive) to `to` (exclusive) that may wrap midnight (#wraps?), or a
35
+ # broken one (#broken?: one hour missing, an hour outside HOURS, or
36
+ # `from == to`) that includes no hour.
37
+ #
38
+ # @api private
39
+ HoursWindow = Data.define(:from, :to) do
40
+ def set? = !(from.nil? && to.nil?)
41
+ def complete? = !from.nil? && !to.nil?
42
+ def in_range? = [from, to].compact.all? { |hour| HOURS.cover?(hour) }
43
+ def broken? = set? && !problem.nil?
44
+ def wraps? = complete? && from > to
45
+
46
+ # Why the window cannot be judged: :hours_incomplete, :hours_range,
47
+ # :hours_equal — nil for a window that can be (or none at all).
48
+ #
49
+ # @return [Symbol, nil]
50
+ def problem
51
+ return unless set?
52
+ return :hours_incomplete unless complete?
53
+ return :hours_range unless in_range?
54
+
55
+ :hours_equal if from == to
56
+ end
57
+
58
+ # @param hour [Integer] 0..23
59
+ # @return [Boolean] whether the window includes the hour; never for a
60
+ # broken window or none at all
61
+ def include?(hour)
62
+ return false unless set? && !broken?
63
+
64
+ wraps? ? hour >= from || hour < to : hour >= from && hour < to
65
+ end
66
+ end
67
+
68
+ # The conditions of an object that answers the three readers (a host's
69
+ # own object, a Struct) without including the module.
70
+ #
71
+ # @api private
72
+ Reading = Data.define(:allowed_ips, :allowed_hours_start, :allowed_hours_end) { include ActivationConditions }
73
+
74
+ class << self
75
+ # @param object [#allowed_ips, #allowed_hours_start, #allowed_hours_end]
76
+ # @return [ActivationConditions]
77
+ def of(object)
78
+ return object if object.is_a?(ActivationConditions)
79
+
80
+ Reading.new(allowed_ips: object.allowed_ips, allowed_hours_start: object.allowed_hours_start,
81
+ allowed_hours_end: object.allowed_hours_end)
82
+ end
83
+
84
+ # The entries of an IP list as written: a String with commas, newlines
85
+ # or spaces between the entries, or an Array of such Strings (the API
86
+ # takes both). Blanks are dropped; the entries are not checked.
87
+ #
88
+ # @param value [String, Array<String>, nil]
89
+ # @return [Array<String>]
90
+ def entries(value)
91
+ Array(value).flat_map { |part| part.to_s.split(SEPARATORS) }.reject(&:empty?)
92
+ end
93
+
94
+ # A condition text as a row stores it: nil for text of whitespace
95
+ # alone — any Unicode space, which Ruby reads as blank while SQL's TRIM
96
+ # takes spaces only — so #conditional? and the SQL scopes read it the
97
+ # same. A row stored before keeps its text; SQL reads it as a
98
+ # condition (narrower: the set is selected per request and passes).
99
+ #
100
+ # @param value [String, nil]
101
+ # @return [String, nil]
102
+ def condition_text(value)
103
+ value.is_a?(String) && value.match?(/\A[[:space:]]*\z/) ? nil : value
104
+ end
105
+
106
+ # The address of a request, as the IP list is compared with it: an
107
+ # IPv4-mapped IPv6 address (`::ffff:10.0.0.5`, what a dual-stack
108
+ # server reports for an IPv4 client) as the IPv4 address it holds.
109
+ #
110
+ # @param value [String, IPAddr, nil]
111
+ # @return [IPAddr, nil] nil for anything that is not an address
112
+ def request_address(value)
113
+ return if value.blank?
114
+
115
+ address = value.is_a?(IPAddr) ? value : IPAddr.new(value.to_s)
116
+ address.ipv4_mapped? ? address.native : address
117
+ rescue IPAddr::Error
118
+ nil
119
+ end
120
+
121
+ # @param entry [String]
122
+ # @return [IPAddr, nil] nil for an entry that is not an IP address or a CIDR range
123
+ def ip_range(entry)
124
+ IPAddr.new(entry)
125
+ rescue IPAddr::Error
126
+ nil
127
+ end
128
+ end
129
+
130
+ # @return [Array<String>] the entries of `allowed_ips` as written (.entries)
131
+ def allowed_ip_list
132
+ ActivationConditions.entries(allowed_ips)
133
+ end
134
+
135
+ # @return [Array<IPAddr>] the ranges of the entries that are addresses
136
+ def allowed_ip_ranges
137
+ allowed_ip_list.filter_map { |entry| ActivationConditions.ip_range(entry) }
138
+ end
139
+
140
+ # @return [Array<String>] the entries of `allowed_ips` that are no address
141
+ def invalid_ip_entries
142
+ allowed_ip_list.reject { |entry| ActivationConditions.ip_range(entry) }
143
+ end
144
+
145
+ # Whether the IP list cannot be judged: text that holds no entry, or an
146
+ # entry that is not an address. A blank list is no condition.
147
+ #
148
+ # @return [Boolean]
149
+ def ip_list_broken?
150
+ return false if allowed_ips.blank?
151
+
152
+ allowed_ip_list.empty? || invalid_ip_entries.any?
153
+ end
154
+
155
+ # @return [HoursWindow]
156
+ def hours_window
157
+ HoursWindow.new(from: allowed_hours_start, to: allowed_hours_end)
158
+ end
159
+
160
+ # Whether a condition of the row cannot be judged (see the module).
161
+ #
162
+ # @return [Boolean]
163
+ def conditions_broken?
164
+ ip_list_broken? || hours_window.broken? || session_flag_broken?
165
+ end
166
+
167
+ # A session flag no request can carry: its name holds a control
168
+ # character (a NUL byte, a newline, a tab). The validation refuses one.
169
+ #
170
+ # @return [Boolean]
171
+ def session_flag_broken?
172
+ requires_session_flag.to_s.match?(/[[:cntrl:]]/)
173
+ end
174
+
175
+ # A granting set whose condition nobody can judge: it never applies, with
176
+ # a request context or without one (ConditionalSets, Admin::EffectiveAccess).
177
+ # For an includer that answers `muting?` (PermissionSet, ConditionalSets::Member).
178
+ #
179
+ # @return [Boolean]
180
+ def never_grants?
181
+ !muting? && conditions_broken?
182
+ end
183
+
184
+ # What the validations refuse about the row's conditions, for a stored
185
+ # row too: `[:ip_entry, entry]` per entry that is no address, `[:ip_empty,
186
+ # text]` for a list of separators, `[:flag_unusable, flag]` for a session
187
+ # flag no request can carry, and the window's problem with the window
188
+ # (:hours_incomplete, :hours_range, :hours_equal).
189
+ #
190
+ # @return [Array<Array(Symbol, Object)>]
191
+ def condition_problems
192
+ problems = invalid_ip_entries.map { |entry| [:ip_entry, entry] }
193
+ problems << [:flag_unusable, requires_session_flag] if session_flag_broken?
194
+ problems << [:ip_empty, allowed_ips] if allowed_ips.present? && allowed_ip_list.empty?
195
+ window = hours_window
196
+ problems << [window.problem, window] if window.problem
197
+ problems
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Admin
5
+ # The one definition of the label the admin shows for an action: the
6
+ # label an admin wrote on the ResourceAction row — one the gem generated
7
+ # (PermissionForge::GeneratedText.label?: blank, the key, the key
8
+ # humanized) is none — else, on an English page, Rails' English for the
9
+ # key ("Export csv", what an English host saw before 0.15:
10
+ # FieldLabel.english), else the key verbatim ("export_csv"; `verbatim?`:
11
+ # the view draws it in <code>). An action with a row and one without
12
+ # read the same.
13
+ #
14
+ # @example
15
+ # ActionLabel.for(action).text # => "Export to CSV" (the admin's label); "export_csv" on sq, "Export csv" on en
16
+ # ActionLabel.for(nil, 'sync').verbatim? # => true on a page that is not English
17
+ #
18
+ # @api private
19
+ module ActionLabel
20
+ # @param action [ResourceAction, #label, nil] the row, when there is one
21
+ # @param key [String, Symbol] the action key (the row's name by default)
22
+ # @return [FieldLabel::Label]
23
+ def self.for(action, key = action&.name)
24
+ key = key.to_s
25
+ label = FieldLabel.written_label(action, key)
26
+ return FieldLabel::Label.new(text: label, verbatim: false) if label
27
+
28
+ english = FieldLabel.english(key)
29
+ english ? FieldLabel::Label.new(text: english, verbatim: false) : FieldLabel::Label.new(text: key, verbatim: true)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -24,6 +24,8 @@ module PermissionForge
24
24
  delegate :effect, :permitted_by, to: :standing
25
25
 
26
26
  def configured? = !action.nil?
27
+ # The label is the key itself (ActionLabel): drawn once, in <code>.
28
+ def verbatim? = label == name
27
29
  end
28
30
 
29
31
  # What one action means right now for people with this set, and which
@@ -147,7 +149,7 @@ module PermissionForge
147
149
 
148
150
  def build_row(key, action)
149
151
  endpoints = endpoints_by_key.fetch(key, [])
150
- Row.new(name: key, label: action&.display_name || key.humanize, action: action, endpoints: endpoints,
152
+ Row.new(name: key, label: ActionLabel.for(action, key).text, action: action, endpoints: endpoints,
151
153
  unlinked: discovery? && endpoints.empty?,
152
154
  standing: Standing.new(key, action, across: @across, configuration: @configuration))
153
155
  end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PermissionForge
4
+ module Admin
5
+ # How the admin names who made a change — the one definition the record
6
+ # footers, the event page, the audit tabs and their CSV, "Assigned by" and
7
+ # the event sentences use: a label the gem stored as a code
8
+ # (Audit::ActorCodes: an API client nobody named, the boot) in the
9
+ # page's language, else the event's actor label (with its kind where
10
+ # it is not a person: "admin (password prompt)"), else "Person #<id>",
11
+ # else the actor kind's word (StateLabel kind :actor: "System",
12
+ # "Unknown"). A record with no event and no created_by / updated_by is
13
+ # "System" (#system).
14
+ #
15
+ # @api private
16
+ module ActorLabel
17
+ module_function
18
+
19
+ # @param event [PermissionForge::AuditEvent]
20
+ # @return [String]
21
+ def for(event)
22
+ type = event.actor_type.to_s
23
+ label = event.actor_label.presence
24
+ code = PermissionForge::Audit::ActorCodes.code(type, label)
25
+ return I18n.t("permission_forge.audit_log.actor_codes.#{code}") if code
26
+ return I18n.t("permission_forge.audit_log.actor_named.#{type}", name: label, default: label) if label
27
+ return I18n.t('permission_forge.audit_log.person_id', id: event.actor_id) if type == 'user' && event.actor_id.present?
28
+
29
+ type_label(type)
30
+ end
31
+
32
+ # The word of an actor kind; a kind outside the catalogue as stored.
33
+ #
34
+ # @param type [String, Symbol]
35
+ # @return [String]
36
+ def type_label(type) = StateLabel.word(:actor, type, default: type.to_s)
37
+
38
+ # Whether the event names nobody: its actor kind is unknown and it has no label.
39
+ #
40
+ # @param event [PermissionForge::AuditEvent, nil]
41
+ # @return [Boolean]
42
+ def unknown?(event)
43
+ !event.nil? && event.actor_type.to_s == 'unknown' && event.actor_label.blank?
44
+ end
45
+
46
+ # Who a record with no event and no person stamped on it was changed by.
47
+ #
48
+ # @return [String]
49
+ def system = StateLabel.word(:actor, :system)
50
+ end
51
+ end
52
+ end