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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +2372 -0
- data/README.md +76 -25
- data/app/controllers/concerns/permission_forge/api/crud_actions.rb +3 -2
- data/app/controllers/concerns/permission_forge/api/input_shapes.rb +200 -0
- data/app/controllers/concerns/permission_forge/api/pagination.rb +12 -5
- data/app/controllers/concerns/permission_forge/api/response_renderer.rb +37 -1
- data/app/controllers/concerns/permission_forge/api_authentication.rb +13 -2
- data/app/controllers/concerns/permission_forge/cache_confirmation.rb +42 -11
- data/app/controllers/concerns/permission_forge/change_review.rb +42 -0
- data/app/controllers/concerns/permission_forge/copy_refusal.rb +39 -0
- data/app/controllers/concerns/permission_forge/param_shapes.rb +207 -7
- data/app/controllers/concerns/permission_forge/save_notice.rb +69 -0
- data/app/controllers/permission_forge/api/audit_events_controller.rb +1 -1
- data/app/controllers/permission_forge/api/base_controller.rb +163 -12
- data/app/controllers/permission_forge/api/dashboard_controller.rb +29 -4
- data/app/controllers/permission_forge/api/permission_set_group_users_controller.rb +5 -2
- data/app/controllers/permission_forge/api/permission_set_groups_controller.rb +5 -4
- data/app/controllers/permission_forge/api/permission_set_rules_controller.rb +7 -5
- data/app/controllers/permission_forge/api/permission_set_users_controller.rb +80 -19
- data/app/controllers/permission_forge/api/permission_sets_controller.rb +12 -6
- data/app/controllers/permission_forge/api/resource_actions_controller.rb +1 -1
- data/app/controllers/permission_forge/api/resource_fields_controller.rb +3 -4
- data/app/controllers/permission_forge/api/resources_controller.rb +26 -12
- data/app/controllers/permission_forge/api/unmatched_routes_controller.rb +20 -0
- data/app/controllers/permission_forge/application_controller.rb +39 -5
- data/app/controllers/permission_forge/assignments_controller.rb +36 -34
- data/app/controllers/permission_forge/bulk_assignments_controller.rb +8 -4
- data/app/controllers/permission_forge/field_lookups_controller.rb +1 -1
- data/app/controllers/permission_forge/maintenance_controller.rb +6 -2
- data/app/controllers/permission_forge/people_controller.rb +14 -14
- data/app/controllers/permission_forge/permission_set_editor.rb +20 -6
- data/app/controllers/permission_forge/permission_set_group_users_controller.rb +3 -1
- data/app/controllers/permission_forge/permission_set_groups_controller.rb +23 -9
- data/app/controllers/permission_forge/permission_set_rules_controller.rb +20 -10
- data/app/controllers/permission_forge/permission_sets_controller.rb +23 -24
- data/app/controllers/permission_forge/resource_actions_controller.rb +11 -6
- data/app/controllers/permission_forge/resource_fields_controller.rb +37 -19
- data/app/controllers/permission_forge/resources_controller.rb +12 -11
- data/app/frontend/permission_forge/pf_admin.css +99 -17
- data/app/frontend/permission_forge/pf_admin.js +36 -0
- data/app/helpers/permission_forge/application_helper.rb +22 -72
- data/app/helpers/permission_forge/audit_attribution_helper.rb +77 -0
- data/app/helpers/permission_forge/audit_helper.rb +57 -119
- data/app/helpers/permission_forge/audit_records_helper.rb +103 -0
- data/app/helpers/permission_forge/audit_sentences_helper.rb +56 -2
- data/app/helpers/permission_forge/badges_helper.rb +74 -17
- data/app/helpers/permission_forge/changes_helper.rb +24 -6
- data/app/helpers/permission_forge/conditions_helper.rb +119 -0
- data/app/helpers/permission_forge/labels_helper.rb +134 -0
- data/app/helpers/permission_forge/people_helper.rb +11 -19
- data/app/helpers/permission_forge/rule_problems_helper.rb +73 -0
- data/app/helpers/permission_forge/sentences_helper.rb +119 -98
- data/app/helpers/permission_forge/settings_helper.rb +2 -2
- data/app/helpers/permission_forge/ui_helper.rb +65 -4
- data/app/models/permission_forge/audit_event.rb +5 -1
- data/app/models/permission_forge/permission_set.rb +208 -18
- data/app/models/permission_forge/permission_set_group.rb +10 -4
- data/app/models/permission_forge/permission_set_group_user.rb +3 -1
- data/app/models/permission_forge/permission_set_rule.rb +254 -17
- data/app/models/permission_forge/permission_set_user.rb +38 -3
- data/app/models/permission_forge/resource.rb +18 -8
- data/app/models/permission_forge/resource_action.rb +15 -6
- data/app/models/permission_forge/resource_field.rb +97 -11
- data/app/views/permission_forge/assignments/confirm_destroy.html.erb +4 -4
- data/app/views/permission_forge/assignments/new.html.erb +10 -6
- data/app/views/permission_forge/audit_events/index.html.erb +1 -1
- data/app/views/permission_forge/audit_events/show.html.erb +16 -9
- data/app/views/permission_forge/bulk_assignments/confirm_revoke.html.erb +3 -3
- data/app/views/permission_forge/bulk_assignments/new.html.erb +3 -3
- data/app/views/permission_forge/field_lookups/show.html.erb +2 -2
- data/app/views/permission_forge/home/_broken_rules_card.html.erb +35 -0
- data/app/views/permission_forge/home/_conditions_card.html.erb +28 -0
- data/app/views/permission_forge/home/_ending_card.html.erb +11 -5
- data/app/views/permission_forge/home/_expired_card.html.erb +1 -1
- data/app/views/permission_forge/home/_mask_strategies_card.html.erb +27 -0
- data/app/views/permission_forge/home/_privacy_card.html.erb +21 -7
- data/app/views/permission_forge/home/_rules_card.html.erb +1 -1
- data/app/views/permission_forge/home/_secret_rules_card.html.erb +33 -0
- data/app/views/permission_forge/home/_stale_card.html.erb +7 -5
- data/app/views/permission_forge/home/_tidy_card.html.erb +1 -1
- data/app/views/permission_forge/home/index.html.erb +5 -6
- data/app/views/permission_forge/people/_access_tab.html.erb +26 -19
- data/app/views/permission_forge/people/_audit_tab.html.erb +8 -8
- data/app/views/permission_forge/people/_effective_field_source.html.erb +13 -7
- data/app/views/permission_forge/people/_effective_tab.html.erb +16 -15
- data/app/views/permission_forge/people/_effective_why.html.erb +20 -16
- data/app/views/permission_forge/people/_why_item.html.erb +1 -1
- data/app/views/permission_forge/people/confirm_remove_all.html.erb +3 -3
- data/app/views/permission_forge/people/erasure_preview.html.erb +1 -1
- data/app/views/permission_forge/permission_set_groups/confirm_destroy.html.erb +3 -3
- data/app/views/permission_forge/permission_set_groups/index.html.erb +3 -3
- data/app/views/permission_forge/permission_set_groups/show.html.erb +28 -19
- data/app/views/permission_forge/permission_set_rules/_builder.html.erb +35 -15
- data/app/views/permission_forge/permission_set_rules/_preview.html.erb +17 -0
- data/app/views/permission_forge/permission_set_rules/confirm_destroy.html.erb +3 -3
- data/app/views/permission_forge/permission_set_rules/preview.html.erb +12 -0
- data/app/views/permission_forge/permission_sets/_assignments_tab.html.erb +37 -22
- data/app/views/permission_forge/permission_sets/_audit_tab.html.erb +4 -4
- data/app/views/permission_forge/permission_sets/_condition_problems.html.erb +15 -0
- data/app/views/permission_forge/permission_sets/_conditions_tab.html.erb +14 -8
- data/app/views/permission_forge/permission_sets/_header.html.erb +6 -5
- data/app/views/permission_forge/permission_sets/_records_summary.html.erb +2 -2
- data/app/views/permission_forge/permission_sets/_resources_tab.html.erb +4 -4
- data/app/views/permission_forge/permission_sets/confirm_destroy.html.erb +4 -4
- data/app/views/permission_forge/permission_sets/confirm_switch_mode.html.erb +3 -3
- data/app/views/permission_forge/permission_sets/index.html.erb +6 -6
- data/app/views/permission_forge/permission_sets/new.html.erb +2 -2
- data/app/views/permission_forge/permission_sets/review.html.erb +13 -3
- data/app/views/permission_forge/permission_sets/show.html.erb +7 -5
- data/app/views/permission_forge/resource_actions/_across_sets.html.erb +7 -10
- data/app/views/permission_forge/resource_actions/_effect.html.erb +5 -5
- data/app/views/permission_forge/resource_actions/_form.html.erb +1 -1
- data/app/views/permission_forge/resource_actions/_managed_note.html.erb +5 -3
- data/app/views/permission_forge/resource_actions/confirm_destroy.html.erb +1 -1
- data/app/views/permission_forge/resource_actions/new.html.erb +1 -1
- data/app/views/permission_forge/resource_actions/show.html.erb +2 -2
- data/app/views/permission_forge/resource_fields/_explain.html.erb +19 -15
- data/app/views/permission_forge/resource_fields/show.html.erb +21 -12
- data/app/views/permission_forge/resources/_actions_tab.html.erb +4 -4
- data/app/views/permission_forge/resources/_fields_tab.html.erb +17 -9
- data/app/views/permission_forge/resources/_rules_tab.html.erb +9 -6
- data/app/views/permission_forge/resources/_sync.html.erb +8 -5
- data/app/views/permission_forge/resources/confirm_destroy.html.erb +3 -3
- data/app/views/permission_forge/resources/new.html.erb +3 -2
- data/app/views/permission_forge/resources/show.html.erb +6 -6
- data/app/views/permission_forge/settings/_row.html.erb +2 -2
- data/app/views/permission_forge/settings/show.html.erb +10 -1
- data/app/views/permission_forge/shared/_across_sets.html.erb +8 -9
- data/app/views/permission_forge/shared/_audit_events.html.erb +2 -2
- data/app/views/permission_forge/shared/_audit_footer.html.erb +13 -2
- data/app/views/permission_forge/shared/_crud_cell.html.erb +6 -6
- data/app/views/permission_forge/shared/_mode_banners.html.erb +1 -1
- data/app/views/permission_forge/shared/_savebar.html.erb +4 -2
- data/config/locales/permission_forge.en.yml +480 -69
- data/config/locales/permission_forge.mk.yml +499 -79
- data/config/locales/permission_forge.sq.yml +509 -80
- data/config/routes.rb +38 -23
- data/lib/generators/permission_forge/install/install_generator.rb +21 -0
- data/lib/generators/permission_forge/install/templates/initializer.rb.tt +35 -34
- data/lib/generators/permission_forge/install/templates/migration.rb.tt +5 -5
- data/lib/permission_forge/activation_conditions.rb +200 -0
- data/lib/permission_forge/admin/action_label.rb +33 -0
- data/lib/permission_forge/admin/action_rows.rb +3 -1
- data/lib/permission_forge/admin/actor_label.rb +52 -0
- data/lib/permission_forge/admin/attention_items.rb +152 -31
- data/lib/permission_forge/admin/audit_log.rb +4 -7
- data/lib/permission_forge/admin/audit_trail.rb +33 -17
- data/lib/permission_forge/admin/bulk_people_matcher.rb +32 -14
- data/lib/permission_forge/admin/chain_entries.rb +42 -0
- data/lib/permission_forge/admin/change_set.rb +12 -1
- data/lib/permission_forge/admin/change_set_parts/base.rb +5 -4
- data/lib/permission_forge/admin/change_set_parts/context.rb +3 -1
- data/lib/permission_forge/admin/change_set_parts/field_edit.rb +4 -1
- data/lib/permission_forge/admin/change_set_parts/field_levels.rb +1 -1
- data/lib/permission_forge/admin/change_set_parts/new_actions.rb +1 -1
- data/lib/permission_forge/admin/change_set_parts/new_fields.rb +38 -11
- data/lib/permission_forge/admin/change_set_parts/resource_edit.rb +17 -1
- data/lib/permission_forge/admin/change_set_parts/resource_flags.rb +1 -1
- data/lib/permission_forge/admin/change_set_parts/set_attrs.rb +4 -0
- data/lib/permission_forge/admin/change_set_parts/values.rb +10 -4
- data/lib/permission_forge/admin/column_fields.rb +22 -21
- data/lib/permission_forge/admin/conditions_form.rb +47 -15
- data/lib/permission_forge/admin/controller_catalog.rb +42 -14
- data/lib/permission_forge/admin/editable_lock.rb +83 -3
- data/lib/permission_forge/admin/effective_access.rb +147 -48
- data/lib/permission_forge/admin/effective_fields.rb +75 -67
- data/lib/permission_forge/admin/error_sentence.rb +47 -0
- data/lib/permission_forge/admin/expiry_presets.rb +17 -0
- data/lib/permission_forge/admin/exports/holdings.rb +6 -3
- data/lib/permission_forge/admin/field_across_sets.rb +7 -4
- data/lib/permission_forge/admin/field_chain.rb +109 -0
- data/lib/permission_forge/admin/field_detail.rb +24 -11
- data/lib/permission_forge/admin/field_label.rb +109 -0
- data/lib/permission_forge/admin/field_level.rb +4 -68
- data/lib/permission_forge/admin/field_provenance.rb +132 -0
- data/lib/permission_forge/admin/field_rows.rb +26 -11
- data/lib/permission_forge/admin/last_changes.rb +14 -0
- data/lib/permission_forge/admin/list_sentence.rb +32 -0
- data/lib/permission_forge/admin/mask_strategy_choices.rb +57 -0
- data/lib/permission_forge/admin/model_catalog.rb +83 -39
- data/lib/permission_forge/admin/model_label.rb +66 -0
- data/lib/permission_forge/admin/people_candidates.rb +21 -9
- data/lib/permission_forge/admin/people_directory.rb +23 -5
- data/lib/permission_forge/admin/permission_set_factory.rb +6 -15
- data/lib/permission_forge/admin/person_label.rb +105 -16
- data/lib/permission_forge/admin/prefill.rb +92 -0
- data/lib/permission_forge/admin/record_name.rb +45 -0
- data/lib/permission_forge/admin/rule_builder.rb +77 -3
- data/lib/permission_forge/admin/rule_form.rb +44 -10
- data/lib/permission_forge/admin/rule_form_input.rb +27 -11
- data/lib/permission_forge/admin/rule_notes.rb +43 -0
- data/lib/permission_forge/admin/rule_preview.rb +96 -0
- data/lib/permission_forge/admin/sensitive_columns.rb +10 -62
- data/lib/permission_forge/admin/settings_catalog.rb +64 -12
- data/lib/permission_forge/admin/state_label.rb +97 -0
- data/lib/permission_forge/admin_method_override.rb +44 -0
- data/lib/permission_forge/api/schema.rb +322 -0
- data/lib/permission_forge/api/serializers/base_serializer.rb +87 -1
- data/lib/permission_forge/api/serializers/permission_set_group_user_serializer.rb +1 -1
- data/lib/permission_forge/api/serializers/permission_set_rule_serializer.rb +1 -1
- data/lib/permission_forge/api/serializers/permission_set_serializer.rb +5 -2
- data/lib/permission_forge/api/serializers/permission_set_user_serializer.rb +1 -1
- data/lib/permission_forge/api/serializers/resource_action_serializer.rb +1 -1
- data/lib/permission_forge/api/serializers/resource_field_serializer.rb +1 -1
- data/lib/permission_forge/api/serializers/resource_serializer.rb +1 -1
- data/lib/permission_forge/api_authorizations.rb +59 -0
- data/lib/permission_forge/api_only_admin.rb +81 -0
- data/lib/permission_forge/audit/actor_codes.rb +32 -0
- data/lib/permission_forge/audit/actor_resolver.rb +1 -2
- data/lib/permission_forge/audit/boot_warnings.rb +29 -9
- data/lib/permission_forge/audit/event_catalog.rb +16 -6
- data/lib/permission_forge/audit/event_query.rb +3 -3
- data/lib/permission_forge/audit/reason_codes.rb +25 -0
- data/lib/permission_forge/audit/runtime.rb +9 -2
- data/lib/permission_forge/audit/settings_observer.rb +2 -2
- data/lib/permission_forge/audit/unmasked_without_user.rb +169 -0
- data/lib/permission_forge/audit.rb +24 -10
- data/lib/permission_forge/boot_checks.rb +180 -63
- data/lib/permission_forge/cache/base.rb +10 -0
- data/lib/permission_forge/cache/null_cache.rb +5 -0
- data/lib/permission_forge/cache/outcome.rb +119 -0
- data/lib/permission_forge/cache/rails_cache.rb +6 -0
- data/lib/permission_forge/cache/report.rb +150 -0
- data/lib/permission_forge/cache/user_keys.rb +8 -4
- data/lib/permission_forge/cache_warnings.rb +140 -0
- data/lib/permission_forge/concerns/auto_permission_scoping.rb +19 -2
- data/lib/permission_forge/concerns/has_tenant_scoping.rb +1 -1
- data/lib/permission_forge/concerns/has_uuid.rb +1 -1
- data/lib/permission_forge/concerns/keeps_copied_values.rb +48 -0
- data/lib/permission_forge/concerns/masked_attributes/alias_access.rb +12 -2
- data/lib/permission_forge/concerns/masked_attributes/virtual_readers.rb +6 -4
- data/lib/permission_forge/concerns/masked_attributes.rb +150 -34
- data/lib/permission_forge/concerns/normalizes_label.rb +78 -0
- data/lib/permission_forge/concerns/permission_cache_accessor.rb +3 -3
- data/lib/permission_forge/concerns/permission_scoped_findable.rb +121 -29
- data/lib/permission_forge/concerns/permissioned_user.rb +1 -1
- data/lib/permission_forge/concerns/records_audit_events.rb +13 -6
- data/lib/permission_forge/concerns/requires_name.rb +37 -0
- data/lib/permission_forge/concerns/validates_column_lengths.rb +24 -0
- data/lib/permission_forge/concerns/validates_dependency.rb +5 -2
- data/lib/permission_forge/concerns/validates_required_columns.rb +74 -0
- data/lib/permission_forge/conditional_sets.rb +13 -6
- data/lib/permission_forge/configuration/admin_ui_settings.rb +18 -4
- data/lib/permission_forge/configuration/audit_settings.rb +2 -1
- data/lib/permission_forge/configuration/controller_action_settings.rb +5 -4
- data/lib/permission_forge/configuration/masked_attributes_settings.rb +73 -19
- data/lib/permission_forge/configuration/presets.rb +20 -11
- data/lib/permission_forge/configuration/query_scoping_settings.rb +55 -0
- data/lib/permission_forge/configuration/strategy_values.rb +2 -1
- data/lib/permission_forge/configuration.rb +41 -18
- data/lib/permission_forge/controller_actions/managed_index.rb +8 -7
- data/lib/permission_forge/database_reach.rb +93 -0
- data/lib/permission_forge/deprecation.rb +1 -1
- data/lib/permission_forge/docs/api_samples.rb +250 -0
- data/lib/permission_forge/docs/configuration_reference.rb +172 -0
- data/lib/permission_forge/encrypted_attributes.rb +150 -0
- data/lib/permission_forge/engine.rb +46 -14
- data/lib/permission_forge/execution_state/restore.rb +74 -0
- data/lib/permission_forge/execution_state.rb +8 -1
- data/lib/permission_forge/field_level.rb +123 -0
- data/lib/permission_forge/field_names.rb +145 -0
- data/lib/permission_forge/field_variants.rb +96 -0
- data/lib/permission_forge/generated_text.rb +95 -0
- data/lib/permission_forge/inspection_filters.rb +83 -0
- data/lib/permission_forge/managed_fields.rb +66 -0
- data/lib/permission_forge/masked_models.rb +25 -19
- data/lib/permission_forge/masking_sync.rb +100 -25
- data/lib/permission_forge/model_bindings.rb +30 -19
- data/lib/permission_forge/model_lookup.rb +42 -0
- data/lib/permission_forge/operators/encrypted_alias.rb +67 -0
- data/lib/permission_forge/operators/registry.rb +6 -0
- data/lib/permission_forge/operators/rule_column.rb +242 -27
- data/lib/permission_forge/operators/rule_path.rb +179 -0
- data/lib/permission_forge/operators/rule_problem.rb +309 -0
- data/lib/permission_forge/operators/rule_sources.rb +96 -0
- data/lib/permission_forge/operators/value_casters/postgresql_types.rb +237 -0
- data/lib/permission_forge/operators/value_casters/temporal.rb +9 -20
- data/lib/permission_forge/operators/value_casters.rb +61 -13
- data/lib/permission_forge/operators/value_typing.rb +83 -7
- data/lib/permission_forge/query_scoping_warnings.rb +22 -17
- data/lib/permission_forge/rake_audit.rb +5 -4
- data/lib/permission_forge/rake_dry_run.rb +32 -0
- data/lib/permission_forge/rake_output.rb +189 -0
- data/lib/permission_forge/record_read_access.rb +51 -0
- data/lib/permission_forge/resource_chain.rb +14 -2
- data/lib/permission_forge/schema_read/ordered_types.rb +72 -0
- data/lib/permission_forge/schema_read.rb +335 -0
- data/lib/permission_forge/secret_columns.rb +49 -0
- data/lib/permission_forge/sensitive_columns.rb +114 -0
- data/lib/permission_forge/services/attribute_access_guard.rb +22 -14
- data/lib/permission_forge/services/bulk_permission_set_assignment.rb +16 -8
- data/lib/permission_forge/services/concerns/clonable.rb +9 -0
- data/lib/permission_forge/services/concerns/settles_cache.rb +58 -0
- data/lib/permission_forge/services/consent_withdrawal_handler.rb +37 -19
- data/lib/permission_forge/services/data_subject_erasure.rb +83 -45
- data/lib/permission_forge/services/expired_assignment_cleaner.rb +62 -25
- data/lib/permission_forge/services/extra_field_defaults.rb +1 -1
- data/lib/permission_forge/services/field_permission_merger.rb +44 -17
- data/lib/permission_forge/services/filters_response_attributes.rb +20 -5
- data/lib/permission_forge/services/gdpr_field_auditor/text_report.rb +33 -5
- data/lib/permission_forge/services/gdpr_field_auditor.rb +68 -48
- data/lib/permission_forge/services/host_key_guard.rb +8 -5
- data/lib/permission_forge/services/mask_value.rb +72 -11
- data/lib/permission_forge/services/permission_cache_clearer.rb +36 -15
- data/lib/permission_forge/services/permission_hash_builder.rb +48 -19
- data/lib/permission_forge/services/permission_rule_applier.rb +131 -68
- data/lib/permission_forge/services/permission_set_cloner.rb +32 -8
- data/lib/permission_forge/services/permission_set_condition_evaluator.rb +38 -36
- data/lib/permission_forge/services/permission_set_group_cloner.rb +2 -1
- data/lib/permission_forge/services/permits_attributes/result.rb +31 -0
- data/lib/permission_forge/services/permits_attributes.rb +48 -27
- data/lib/permission_forge/services/resource_field_generator/row_builder.rb +96 -0
- data/lib/permission_forge/services/resource_field_generator.rb +71 -42
- data/lib/permission_forge/services/resource_with_fields_builder.rb +2 -1
- data/lib/permission_forge/services/rls_operator_sql.rb +83 -24
- data/lib/permission_forge/services/rls_policy_generator.rb +83 -39
- data/lib/permission_forge/services/rls_read_access_sql.rb +131 -0
- data/lib/permission_forge/services/security_audit/action_check.rb +49 -5
- data/lib/permission_forge/services/security_audit/assignment_check.rb +2 -1
- data/lib/permission_forge/services/security_audit/audit_trail_check.rb +1 -3
- data/lib/permission_forge/services/security_audit/conditions_check.rb +92 -0
- data/lib/permission_forge/services/security_audit/config_check.rb +41 -18
- data/lib/permission_forge/services/security_audit/integrity_check.rb +71 -4
- data/lib/permission_forge/services/security_audit/masking_without_user.rb +66 -0
- data/lib/permission_forge/services/security_audit/personal_data_check.rb +7 -11
- data/lib/permission_forge/services/security_audit/rule_check.rb +180 -50
- data/lib/permission_forge/services/security_audit/text_report.rb +14 -2
- data/lib/permission_forge/services/security_audit/unmanaged_actions.rb +145 -0
- data/lib/permission_forge/services/security_audit/user_model_check.rb +4 -3
- data/lib/permission_forge/services/security_audit.rb +16 -4
- data/lib/permission_forge/services/stale_assignment_auditor.rb +8 -5
- data/lib/permission_forge/timestamp_fields.rb +60 -0
- data/lib/permission_forge/unique_conflict.rb +117 -0
- data/lib/permission_forge/version.rb +1 -1
- data/lib/permission_forge/virtual_fields/discovery.rb +2 -1
- data/lib/permission_forge/virtual_fields.rb +15 -5
- data/lib/permission_forge/warnings.rb +263 -0
- data/lib/permission_forge/web_request.rb +248 -0
- data/lib/permission_forge.rb +54 -0
- data/lib/tasks/permission_forge.rake +26 -6
- data/lib/tasks/permission_forge_audit_events.rake +8 -4
- data/lib/tasks/permission_forge_cache.rake +12 -12
- data/lib/tasks/permission_forge_docs.rake +48 -0
- data/lib/tasks/permission_forge_expiration.rake +11 -5
- data/lib/tasks/permission_forge_gdpr.rake +36 -20
- metadata +84 -1
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,2378 @@ defaults, and every such change is listed under **Changed** with its opt-out.
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.15.0] - 2026-10-19
|
|
10
|
+
|
|
11
|
+
Upgrading from 0.14 is the three steps of the
|
|
12
|
+
[migration guide's checklist](https://github.com/fetijashari/permission_forge/blob/main/docs/migration-guide.md#upgrading-to-015):
|
|
13
|
+
no generator to run (`permission_forge:upgrade` is for installs older than 0.13), `bin/rails permission_forge:audit`
|
|
14
|
+
before the deploy — it lists the stored rows this release reads differently (rules that match nothing, broken
|
|
15
|
+
activation conditions, variant rows, unknown mask strategies) — and an RLS re-sync after it. From 0.13, see
|
|
16
|
+
[From 0.13](https://github.com/fetijashari/permission_forge/blob/main/docs/migration-guide.md#from-013): a revocation
|
|
17
|
+
made on 0.15 does not reach what 0.13 processes have cached. Most Changed
|
|
18
|
+
entries need nothing from you. These may need a change in your app or its clients: **A mapped controller action no
|
|
19
|
+
Permission Set manages is refused (403) unless `unconfigured_action_strategy = :allow`.** (manage the actions the
|
|
20
|
+
audit lists as `unmanaged_actions_refused` — with tenancy, in each tenant named — or opt out), **An unrescued denial answers 403 Forbidden instead of 500.**
|
|
21
|
+
(add `public/403.html`, or rescue the errors), **RLS: `RlsPolicyGenerator::GENERATOR_VERSION` is 7** (run `permission_forge:rls:sync` after the deploy), **`include_nulls` is off unless asked for — in code, through
|
|
22
|
+
the JSON API and in the admin's rule builder alike.** (pass `include_nulls: true` where a rule should let empty values
|
|
23
|
+
through), **A refusing `api_auth` answers 401 Unauthorized instead of 403.** and **API: a value of the wrong shape for
|
|
24
|
+
its attribute is a 422 keyed by it, never dropped or cast.** (API clients), **A Permission Set, a group or a rule
|
|
25
|
+
created without a name — or renamed to a blank one — is refused, and a rule created without a blocked message stores
|
|
26
|
+
none.** (code and API clients that create without a name, or with a blank one), **More warnings are SECURITY warnings.** (log alerts that match the warning text), and **`config.preset = :strict` sets
|
|
27
|
+
`masked_attributes_nil_user_strategy = :hide_in_requests`, and new installs get it from the generator** (pages that
|
|
28
|
+
read a masked model before sign-in).
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- **`rake permission_forge:audit` reports every kind of rule that matches nothing.** `Operators::RuleProblem::KINDS`
|
|
33
|
+
is the one taxonomy, and each kind lands in exactly one finding: `missing_rule_columns` ("Rules naming a column,
|
|
34
|
+
table or association that does not exist": High while people hold the set, otherwise Medium; set, resource, field,
|
|
35
|
+
operator, value, rule and people per item) lists a rule whose field name is not a column of its table, one whose
|
|
36
|
+
name is qualified with a table this database does not have, one whose field name is not a name (a trailing or
|
|
37
|
+
doubled dot), and a join rule whose `field_accessor_path` names an association its model does not have (`ownr`) or
|
|
38
|
+
one no join can be built through (a polymorphic `belongs_to`, a scope that takes the record, a `:through` or
|
|
39
|
+
`inverse_of` naming nothing, a class that cannot be loaded); the new `unknown_rule_operators` (High while held,
|
|
40
|
+
otherwise Medium) lists rules with an operator or join type this version does not know (written by a newer version
|
|
41
|
+
in a rolling deploy, by another application on the shared tables, or past the model); the new
|
|
42
|
+
`foreign_table_names` (Info) lists rules qualified with a real table of another model, which are left to the
|
|
43
|
+
database. Run it before upgrading.
|
|
44
|
+
<!-- pin: test/permission_forge/services/security_audit_test.rb#a rule naming a column its table does not have is a high finding while people hold the set -->
|
|
45
|
+
<!-- pin: test/permission_forge/services/security_audit_test.rb#a rule whose association path cannot be followed is a high finding while people hold the set -->
|
|
46
|
+
<!-- pin: test/permission_forge/services/security_audit_rule_check_test.rb#each kind lands in exactly one finding -->
|
|
47
|
+
- **Home lists the rules that match nothing.** A "Rules that match nothing" card on the admin's Home lists every active
|
|
48
|
+
rule in effect that the audit lists under `missing_rule_columns`, `unknown_rule_operators` or `unusable_rule_values`
|
|
49
|
+
(`AttentionItems#broken_rules`, by `Operators::RuleProblem`) — a missing column, table or relationship, a condition
|
|
50
|
+
or a way of joining this version does not know, a value the field cannot hold — with why, the name as stored
|
|
51
|
+
("Matches nothing: `stauts` is not a column of Customer"), how many people hold the set, and **Fix rule** (the
|
|
52
|
+
rule's edit page; for a locked set the Locked badge and **Open rules**). The Rules tab, the rule's edit page and a
|
|
53
|
+
person's effective access say the same, in every locale.
|
|
54
|
+
<!-- pin: test/permission_forge/html/broken_rules_admin_test.rb#Home lists each rule in effect that matches nothing, with why, linking to the rule -->
|
|
55
|
+
<!-- pin: test/permission_forge/html/broken_rules_admin_test.rb#Home, the Rules tab and the edit page say why, for every kind, in every locale -->
|
|
56
|
+
- **`rake permission_forge:audit` reports fields naming nothing on their model** (`integrity/unknown_fields`). A
|
|
57
|
+
`ResourceField` named `zzz` (or `emial`) on a `Customer` Resource was saved — 201 through the API — and configured
|
|
58
|
+
nothing, so a typo went unnoticed. The audit now lists the rows in effect whose name is none of: a column of the
|
|
59
|
+
model's table (read again before it is listed), an attribute or `alias_attribute`, a declared or discovered virtual
|
|
60
|
+
field, a public reader or writer, or a configured variant (`field_variant_mappings`) of one of those or of another
|
|
61
|
+
row of the Resource — on the model or on a loaded single-table-inheritance subclass of it. Low when a muting set
|
|
62
|
+
holds one (it mutes nothing it was meant to), otherwise Info; set, resource, field and people per row; Resources
|
|
63
|
+
whose model the app cannot load are skipped. Saving still accepts such a name: `FiltersResponseAttributes` filters
|
|
64
|
+
any key of a hash, so a key only a serializer computes is configured by a row of its name — declare it in
|
|
65
|
+
`extra_fields_for_permissions` to say it is meant.
|
|
66
|
+
<!-- pin: test/permission_forge/services/security_audit_unknown_fields_test.rb#a row naming nothing on its model still saves, and the audit lists it (info for a granting set) -->
|
|
67
|
+
<!-- pin: test/permission_forge/services/security_audit_unknown_fields_test.rb#a muting set holding one makes it low: it mutes nothing -->
|
|
68
|
+
- **`rake permission_forge:audit` reports fields masked with a strategy this app does not know**
|
|
69
|
+
(`integrity/unknown_mask_strategies`: High while people hold the set, otherwise Medium — a row written past the
|
|
70
|
+
model, a blank, or a custom strategy since removed from `config.custom_mask_strategies`). Such a field is masked
|
|
71
|
+
fully until the row or the configuration is fixed.
|
|
72
|
+
<!-- pin: test/permission_forge/services/security_audit_integrity_check_test.rb#a field in effect with a mask strategy this app does not know is reported, high while people hold the set -->
|
|
73
|
+
- **`rake permission_forge:audit` reports `secret_rule_columns`** (Medium): record rules in effect that compare a
|
|
74
|
+
password digest, a token, an `encrypted_*` column or a secret (`config.rule_secret_column_patterns`, by name; an
|
|
75
|
+
attribute declared with `encrypts` is `encrypted_rule_columns`, under Changed). The item leaves the rule value out.
|
|
76
|
+
<!-- pin: test/permission_forge/admin/attention_parity_test.rb#secret_rule_columns: the audit's finding is on a Home card (secret_rules) -->
|
|
77
|
+
- **`rake permission_forge:audit` has a `conditions` check**: `incomplete_conditions` (Medium — stored activation
|
|
78
|
+
conditions nobody can judge: an IP entry that is not an address, a list with no entry, the same start and end hour,
|
|
79
|
+
one hour of the two, an hour outside 0–23 — with what each set does now) and `variant_rows_conflict` (Info — a row
|
|
80
|
+
next to an explicit row for one of its variants; the explicit row decides).
|
|
81
|
+
<!-- pin: test/permission_forge/services/security_audit_conditions_check_test.rb#incomplete_conditions lists every stored row the validation would refuse, saying what each does now -->
|
|
82
|
+
- **Three more Home cards for what the security audit reports**: fields masked with a strategy this app does not
|
|
83
|
+
know (`unknown_mask_strategies`, with a link to the field drawer), Permission Sets whose activation conditions nobody
|
|
84
|
+
can judge (`incomplete_conditions`, with a link to the Conditions tab) and record rules that compare a secret column
|
|
85
|
+
(`secret_rule_columns`). Home and the audit read the same rows (`ResourceField.with_unknown_mask_strategy`,
|
|
86
|
+
`PermissionSet.with_broken_conditions`, `AttentionItems#secret_rules`).
|
|
87
|
+
<!-- pin: test/permission_forge/admin/attention_parity_test.rb#unknown_mask_strategies: the audit's finding is on a Home card (unknown_mask_strategies) -->
|
|
88
|
+
- **`config.rule_secret_column_patterns`** — the column names a record rule may not compare
|
|
89
|
+
(`PermissionForge::SecretColumns`, matched against the column name in lower case; default
|
|
90
|
+
`[/\Apassword_digest\z/, /_digest\z/, /token/, /\Aencrypted_/, /_secret/]`). Listed on the Settings page (Record
|
|
91
|
+
rules) and in the configuration reference. See **Changed** for what it refuses.
|
|
92
|
+
<!-- pin: test/permission_forge/models/permission_set_rule_secret_columns_test.rb#rule_secret_column_patterns takes a list of regular expressions -->
|
|
93
|
+
- **`NotAuthorizedError#failure_messages`.** The `failure_message` of each row-level rule that denied the record, as
|
|
94
|
+
the admin wrote it, in rule order and without repeats (`[]` when no rule denied), so a host can render a friendly 403
|
|
95
|
+
without parsing `message` (`[Forbidden by Rule: <name>] with message: <failure_message>`). Only the rules that deny
|
|
96
|
+
the record on their own are named (each rule is checked against the record on the denial path, one query per rule;
|
|
97
|
+
should no single rule deny — a custom operator that is not a plain filter — the cumulative attribution is kept, so a
|
|
98
|
+
denial never names no rule). A rule whose `failure_message` is blank gives `permission_forge.denials.rule_default`
|
|
99
|
+
in the current locale ("You do not have permission to perform this action."). `message`, `details` and the
|
|
100
|
+
`access.denied` event's `messages` use the same list of rules. See
|
|
101
|
+
[Handling Denials](docs/row-level-rules.md#handling-denials).
|
|
102
|
+
<!-- pin: test/permission_forge/concerns/permission_scoped_findable_test.rb#NotAuthorizedError#failure_messages are the denying rules' own messages, without parsing -->
|
|
103
|
+
<!-- pin: test/permission_forge/concerns/permission_scoped_findable_test.rb#a denial names only the rules that deny the record on their own, in rule order -->
|
|
104
|
+
<!-- pin: test/permission_forge/concerns/permission_scoped_findable_test.rb#a denial no single rule explains falls back to the cumulative attribution, never to no rule -->
|
|
105
|
+
<!-- pin: test/permission_forge/services/rule_blame_property_test.rb#a denial blames exactly the rules that exclude the record on their own, in rule order -->
|
|
106
|
+
- **The install generator prints the next steps.** After the files it lists `bin/rails db:migrate` (not with
|
|
107
|
+
`--skip-migration`), the mount line, where the admin credentials go (`PF_ADMIN_USER` / `PF_ADMIN_PASSWORD`, or your
|
|
108
|
+
own admin controller), `bin/rails permission_forge:audit` and a link to the Quick Start, plus the line that makes
|
|
109
|
+
`can_*?` work: `include PermissionForge::Concerns::PermissionedUser` in your User model. `rails destroy` prints none.
|
|
110
|
+
<!-- pin: test/permission_forge/generators/install_generator_next_steps_test.rb#an install prints the next steps after the files -->
|
|
111
|
+
<!-- pin: test/permission_forge/generators/install_generator_next_steps_test.rb#rails destroy prints no next steps -->
|
|
112
|
+
- **A boot warning in development when the permission cache is per process.** Rails 8.0+ generates
|
|
113
|
+
`config.cache_store = :memory_store` for development (7.1/7.2: after `bin/rails dev:cache`) and the generated
|
|
114
|
+
initializer caches through `Rails.cache`, so a permission change made from a console, `bin/rails runner`, a rake
|
|
115
|
+
task or `db:seed` invalidated only that process's cache: a running server kept the permissions it had cached until
|
|
116
|
+
`cache_ttl` (an hour) or a restart, and the change looked lost. The development boot now says so, with the two
|
|
117
|
+
remedies (`config.cache_store = :file_store, "tmp/cache/store"` in `config/environments/development.rb`, or
|
|
118
|
+
`cache_backend = :null` in development), when `cache_backend` is `:memory` or the store is a `MemoryStore`; file,
|
|
119
|
+
Redis, Solid Cache, Memcached and null stores are not warned about. The generated initializer's comment above
|
|
120
|
+
`cache_backend` and [Cache Consistency](docs/configuration.md#cache-consistency) say so.
|
|
121
|
+
<!-- pin: test/permission_forge/cache_warnings_test.rb#development with Rails.cache as a :memory_store warns, naming what happens and both remedies -->
|
|
122
|
+
<!-- pin: test/permission_forge/cache_warnings_test.rb#the development warning reads right in any process, not only a server -->
|
|
123
|
+
<!-- pin: test/permission_forge/cache_warnings_test.rb#development with a shared or a null store does not warn -->
|
|
124
|
+
- **A boot warning for an API-only application that mounts the engine** (`config.api_only = true`): it names the
|
|
125
|
+
middlewares its admin screens need (`ActionDispatch::Cookies`, `ActionDispatch::Session::CookieStore`,
|
|
126
|
+
`ActionDispatch::Flash`) and the lines to add — without them every admin page that renders a form is a 500; the
|
|
127
|
+
JSON API works without them. Checked once the routes are drawn (it never draws them); an app that uses only the
|
|
128
|
+
API sets `config.api_only_admin_warning = false`.
|
|
129
|
+
<!-- pin: test/permission_forge/boot_checks_api_only_test.rb#the warning goes through the emitter and names the middlewares and the lines to add -->
|
|
130
|
+
<!-- pin: test/permission_forge/boot_checks_api_only_test.rb#the warning comes when the routes are drawn, which the check never does itself -->
|
|
131
|
+
<!-- pin: test/permission_forge/boot_checks_api_only_test.rb#config.api_only_admin_warning = false silences the warning (an app that uses only the JSON API) -->
|
|
132
|
+
- **One cache outcome for every operation that clears cached permissions.** `PermissionForge::Cache::Outcome`
|
|
133
|
+
(`confirmed`, `process_local`, `in_request`, `unconfirmed`), measured by `Cache::Report`, is what the terminal, the
|
|
134
|
+
admin flash, the API body and the summary audit event report: `PermissionCacheClearer`, `DataSubjectErasure`,
|
|
135
|
+
`ExpiredAssignmentCleaner`, `ConsentWithdrawalHandler` and `BulkPermissionSetAssignment` answer it under `cache:`;
|
|
136
|
+
`cache.cleared`, `erasure.performed`, `cleanup.expired_assignments` and `consent.withdrawn` record it in their data
|
|
137
|
+
(`cache.settled` the answer of the last two once their cache work ran); the admin's flashes (one assignment's
|
|
138
|
+
changes and a batch of edits too), `POST /api/permission_sets/:id/users/bulk` (`cache: {…}`) and `POST` / `DELETE
|
|
139
|
+
…/users` (`meta.cache`) say when the store did not confirm and when it lives in one process; inside a caller's
|
|
140
|
+
transaction every one of them reports `confirmed: nil` (`Cache::Report#deferred?`). `process_local` is
|
|
141
|
+
`PermissionForge.cache.process_local?` (`cache_backend = :memory`, or `Rails.cache` a `:memory_store`): there a clear
|
|
142
|
+
from rake reaches the rake process only, which the terminal said and the audit log did not.
|
|
143
|
+
<!-- pin: test/permission_forge/audit/cache_scope_events_test.rb#erasure.performed on the suite's memory store says process-local in the event and the result, in every locale -->
|
|
144
|
+
<!-- pin: test/permission_forge/services/permission_cache_clearer_test.rb#every event says whether the clear reached only this process (a memory store) or a shared store -->
|
|
145
|
+
<!-- pin: test/permission_forge/audit/cache_scope_events_test.rb#cleanup.expired_assignments on a shared store says process_local false and reads without a suffix -->
|
|
146
|
+
- **The audit log says which processes kept their cached permissions.** One suffix per locale for an event whose
|
|
147
|
+
data says `process_local: true`, chosen by where the operation ran and what the store said: in a web server process
|
|
148
|
+
(an admin or API request, a host controller), "The cached permissions were cleared in this server process only (an
|
|
149
|
+
in-memory store): the other web workers and the job processes kept theirs."; from a rake task, runner, console or
|
|
150
|
+
job, "The cached permissions were cleared only in the process that ran it (an in-memory store): every other process
|
|
151
|
+
kept theirs."; when the store did not confirm, "The cache store lives in each process (an in-memory store): the other
|
|
152
|
+
processes keep their cached permissions either way." (`audit_log.cache_scope.server_process` / `own_process` /
|
|
153
|
+
`unconfirmed`, en/sq/mk). On a shared store the sentence reads as before.
|
|
154
|
+
<!-- pin: test/permission_forge/html/audit_sentences_test.rb#a clear made from an admin request names the other workers, not the server itself -->
|
|
155
|
+
<!-- pin: test/permission_forge/html/audit_sentences_test.rb#a cache clear that reached only its own in-memory store says so; one on a shared store reads as before -->
|
|
156
|
+
- **`PermissionForge::RakeOutput`: one presenter for what the rake tasks print.** `rake permission_forge:erasure`,
|
|
157
|
+
`cleanup_expired_assignments`, `cache:clear` and `audit_events:purge` print one line per key of the result their
|
|
158
|
+
summary event records (the cache outcome included: "Cached permissions removed", "Cache scope", "Ran in"), name
|
|
159
|
+
people as the admin does (`Admin::PersonLabel`; "id 7 (not in the people list)" for someone outside
|
|
160
|
+
`user_list_resolver`) instead of `User 7` / `User IDs: 1, 2`, and say "would" on a dry run; `expired_assignments`
|
|
161
|
+
and `stale_assignments` name people the same way. The erasure prints how many audit-log name labels it would clear
|
|
162
|
+
(dry run) or cleared, as actor and as subject, as `erasure.performed` records them;
|
|
163
|
+
`Services::DataSubjectErasure` returns them (`actor_labels_to_clear` / `record_labels_to_clear` in a dry run,
|
|
164
|
+
`actor_labels_cleared` / `record_labels_cleared` otherwise).
|
|
165
|
+
<!-- pin: test/permission_forge/tasks/rake_output_contract_test.rb#permission_forge:erasure prints every key its summary event records, on the memory store with the process-local note -->
|
|
166
|
+
<!-- pin: test/permission_forge/tasks/rake_output_test.rb#the erasure says how many audit-log name labels it cleared -->
|
|
167
|
+
<!-- pin: test/permission_forge/services/data_subject_erasure_test.rb#a dry run counts the name labels to clear, the run returns the ones it cleared -->
|
|
168
|
+
- **`rake -T permission_forge` shows every description whole.** Every task's first sentence is at most 100
|
|
169
|
+
characters, ends on a word (the erasure task's stopped at "(supports GDPR Art"; it now says "Article 17") and the
|
|
170
|
+
description names every environment variable the task reads (`ACTOR=`, `REASON=`, `USER_ID=`, `DRY_RUN=`,
|
|
171
|
+
`FAIL_ON=`, `FORMAT=`, `THRESHOLD_DAYS=`).
|
|
172
|
+
<!-- pin: test/permission_forge/tasks/rake_descriptions_test.rb#permission_forge:erasure: the first sentence shows whole in rake -T -->
|
|
173
|
+
<!-- pin: test/permission_forge/tasks/rake_output_test.rb#the erasure task description is not cut at an abbreviation -->
|
|
174
|
+
- **`config.api_stats_authorization`.** A lambda called with the controller that decides who may read
|
|
175
|
+
`GET /api/stats` while `api_stats_require_admin` is on (the default); a truthy answer allows. It replaces an
|
|
176
|
+
`api_auth` that set `@admin = true` on the controller (see Deprecated). A refusal answers 403 with a body (it had
|
|
177
|
+
none) and is recorded as `api.auth_failed` with reason `stats_not_authorized`; while the lambda is unset the 403
|
|
178
|
+
names it. See [Dashboard Stats](docs/admin-api.md#dashboard-stats).
|
|
179
|
+
<!-- pin: test/permission_forge/api/dashboard_controller_test.rb#api_stats_authorization decides who reads the stats -->
|
|
180
|
+
<!-- pin: test/permission_forge/api/dashboard_controller_test.rb#GET /api/stats with api_stats_require_admin true and no api_stats_authorization is a 403 that names it -->
|
|
181
|
+
- **`PermitsAttributes.call(..., report: true)` says what was dropped.** It answers a `PermitsAttributes::Result`:
|
|
182
|
+
`params` (the filtered Hash, as before), `dropped` (the paths of the dropped keys, never values), `dropped?` and
|
|
183
|
+
`dropped_fields` (the top-level keys), so a controller can say "Not saved: status, notes" instead of a silent
|
|
184
|
+
success. The paths are collected whether or not `params.filtered` is heard; with no model (`resource: nil`) every
|
|
185
|
+
key is reported as dropped. Without `report:` the return value is the Hash, unchanged. See
|
|
186
|
+
[Telling the user what was not saved](docs/field-filtering.md#telling-the-user-what-was-not-saved).
|
|
187
|
+
<!-- pin: test/permission_forge/services/permits_attributes_test.rb#report: true answers the same filtered hash plus the dropped paths -->
|
|
188
|
+
<!-- pin: test/permission_forge/services/permits_attributes_test.rb#report: true with no model to filter for lists every key, with or without a user -->
|
|
189
|
+
- **Bulk assignment reports the ids it skipped.** `POST …/users/bulk` answers `skipped` next to `assigned`, `revoked`
|
|
190
|
+
and `invalidated`: the requested ids that name nobody `user_list_resolver` lists (`[99]`; `[]` when none), so a typo
|
|
191
|
+
in an automated list no longer goes unnoticed. An id outside the resolver and an unknown one read the same.
|
|
192
|
+
<!-- pin: test/permission_forge/api/permission_set_users_controller_test.rb#POST bulk reports the ids it skipped: nobody, or outside user_list_resolver -->
|
|
193
|
+
- **`permission_set_group_id` takes the group's uuid.** The API answers with uuids (`api_record_identifier = :uuid`),
|
|
194
|
+
but `PATCH`/`POST …/permission_sets` read `permission_set_group_id` only as a numeric id: the group's uuid was a 422.
|
|
195
|
+
Under `:uuid` it now takes the uuid or the id; under `:id`, the id. The value is looked up in the current tenant
|
|
196
|
+
like every identifier (see Fixed for a uuid that starts with digits).
|
|
197
|
+
<!-- pin: test/permission_forge/api/record_identifier_test.rb#under :uuid permission_set_group_id takes the group uuid, or its id -->
|
|
198
|
+
<!-- pin: test/permission_forge/api/record_identifier_test.rb#under :id permission_set_group_id takes the id only -->
|
|
199
|
+
- **API `GET /api/permission_sets/:id` answers `allowed_ip_list`** (the entries as the engine reads them) beside
|
|
200
|
+
`allowed_ips`.
|
|
201
|
+
<!-- pin: test/permission_forge/api/permission_set_conditions_api_test.rb#the serializer shows the entries as the engine reads them -->
|
|
202
|
+
- **Cross-set unmasking findings name the masking sets too.** `GdprFieldAuditor`'s `:cross_set_unmasking` finding
|
|
203
|
+
gains `masked_in` (the names of the granting Permission Sets that mask the field), `rake permission_forge:audit_masks`
|
|
204
|
+
prints them ("masked in Permission Set Support") and `rake permission_forge:audit` lists them in parentheses after
|
|
205
|
+
the set that unmasks. The other keys are unchanged.
|
|
206
|
+
<!-- pin: test/permission_forge/services/gdpr_field_auditor_test.rb#a cross-set finding names the sets that mask the field -->
|
|
207
|
+
<!-- pin: test/permission_forge/services/security_audit_test.rb#cross-set unmasking names the set that unmasks and the sets that mask -->
|
|
208
|
+
- **`MaskValue.call!`** (strict: raises for a strategy this app does not know — the admin preview, the validation),
|
|
209
|
+
**`MaskValue.known?`**, and the **`mask_strategy_unknown.permission_forge`** notification (payload `strategy`,
|
|
210
|
+
`fallback`), published once per process per name together with a warning.
|
|
211
|
+
<!-- pin: test/permission_forge/services/mask_value_test.rb#call reports a strategy it does not know once per process per name: a warning and a notification -->
|
|
212
|
+
- **`PermissionForge::ModelLookup.model_for(name)`** — the one definition of "this name is a model with a table"
|
|
213
|
+
(an ActiveRecord class, not abstract, whose table exists; nil for `Kernel`, `String`, an abstract model, a model
|
|
214
|
+
without its table, a constant whose file raises as it loads).
|
|
215
|
+
<!-- pin: test/permission_forge/model_lookup_test.rb#anything that is not a model with a table is nil -->
|
|
216
|
+
- **The JSON API's `POST …/resources/:id/generate_fields` answers 422** `{ "name": ["names no model this app can
|
|
217
|
+
load"] }` for a Resource named after a constant that is no model (`Kernel`), instead of a 500 or the generic
|
|
218
|
+
message.
|
|
219
|
+
<!-- pin: test/permission_forge/api/resources_controller_test.rb#POST generate_fields on a Resource naming no model this app can load is a 422 on name -->
|
|
220
|
+
- **`PermissionForge::ActivationConditions`**: the one reading of a Permission Set's `allowed_ips`
|
|
221
|
+
(`#allowed_ip_list`, `#allowed_ip_ranges`, `#invalid_ip_entries`) and hour window (`#hours_window`: `set?`,
|
|
222
|
+
`wraps?`, `broken?`, `include?`), with `#conditions_broken?` / `#condition_problems`; included in `PermissionSet` and
|
|
223
|
+
`ConditionalSets::Member`, read by the evaluator, the admin Conditions form, the sentences, the API serializer and
|
|
224
|
+
the audit.
|
|
225
|
+
<!-- pin: test/permission_forge/models/permission_set_condition_rows_test.rb#hours_window: none, a window, an overnight window, and the broken shapes -->
|
|
226
|
+
- **`PermissionHashBuilder.granted_flags(resource)`**: the one definition of what a granting Resource row grants after
|
|
227
|
+
its dependencies; **`FieldPermissionMerger.targets_of(rows, variants)`**: the one definition of which names a
|
|
228
|
+
resource's rows write and in what order.
|
|
229
|
+
<!-- pin: test/permission_forge/services/field_permission_merger_test.rb#targets_of lists own names first, then the variants no row of the resource names -->
|
|
230
|
+
- **`PermissionForge::SensitiveColumns`** — the one definition of "this column looks like personal data, with this
|
|
231
|
+
mask": `config.gdpr_field_patterns` first, then the host's `filter_parameters` (strategy `full`) while
|
|
232
|
+
`filter_parameters_integration` is on. `match(name)` answers a `Match` (`pattern`: the alternative that matched or
|
|
233
|
+
`filter_parameters`; `strategy`; `source`). The admin's presets, Sync, Fields tab and Home, the field generator,
|
|
234
|
+
`GdprFieldAuditor` and the boot warning all ask it.
|
|
235
|
+
<!-- pin: test/permission_forge/sensitive_columns_test.rb#matches gdpr patterns with the alternative that matched, its strategy and the source -->
|
|
236
|
+
- **One row builder for generated field rows** — `ResourceFieldGenerator.build(resource:, column:, level:)` /
|
|
237
|
+
`.rows(resource)` (`ResourceFieldGenerator::RowBuilder`, reading the level tables of `PermissionForge::FieldLevel`,
|
|
238
|
+
moved from `Admin::FieldLevel`, which stays as an alias): the flags of the level, the privacy default (under
|
|
239
|
+
`gdpr_defaults`, on a granting set, a sensitive column granted Read-only starts Masked; Editable masks nothing;
|
|
240
|
+
Hidden stays Hidden), Rails' timestamps at most read-only, the recommended mask strategy on every row, the label,
|
|
241
|
+
and no row for a level that is none (Unchanged on a muting set). The admin's presets and "Sync fields"
|
|
242
|
+
(`Admin::ColumnFields`), the Fields tab's bulk add (`Admin::ChangeSet`) and the generator (the API's
|
|
243
|
+
`generate_fields`, `Resource.grant_full_access!`, the console) write the same row for the same level.
|
|
244
|
+
<!-- pin: test/support/decision_parity/fields.rb#(a) Customer, muting=false, gdpr_defaults=true, read: presets, bulk and the generator write the same rows -->
|
|
245
|
+
<!-- pin: test/permission_forge/services/resource_field_generator_gdpr_test.rb#build: the privacy default masks a sensitive column granted Read-only, and only that -->
|
|
246
|
+
- **`ResourceField#exposes_in_clear?`** and the scope **`ResourceField.exposing_in_clear`** — the one definition of
|
|
247
|
+
"readable in the clear" (active, read on, no mask) that the GDPR audit, the security audit's `personal_data`
|
|
248
|
+
check, Home's privacy card and the boot warning ask.
|
|
249
|
+
<!-- pin: test/permission_forge/models/resource_field_in_clear_test.rb#the predicate and the scope agree on every combination of active, read and mask -->
|
|
250
|
+
- **`Admin::EditableLock.locked?(record)` walks the chain and `EditableLock.permits?(record, action)` is the one
|
|
251
|
+
table of what the admin may offer on a locked record** (`:edit`, `:add`, `:toggle`, `:delete` — also the container,
|
|
252
|
+
a set in a locked group —, `:grant` / `:revoke` / `:change_expiry` by direction, `:duplicate` and `:review` always).
|
|
253
|
+
Every view asks it (`pf_locked?`, `pf_lock_badge(record)`); `html/locked_pages_test.rb` renders every page for a
|
|
254
|
+
locked set, resource, field, rule, group and Muting Permission Set and checks every enabled control against it.
|
|
255
|
+
<!-- pin: test/permission_forge/html/locked_pages_test.rb#a locked Permission Set offers no change to what it holds, only duplicate, review and narrowing -->
|
|
256
|
+
- **`PermissionSetUser.ending_within(days)` / `#ending_soon?` / `#stale?(days)` / `.ended?(time)`** and
|
|
257
|
+
**`Admin::AttentionItems.stale_days`** — the one window of "ending soon" (`ENDING_SOON_DAYS`, 7) and the one review
|
|
258
|
+
threshold, read by Home, the People page, the end-date cell, the audit's `stale_assignments` and
|
|
259
|
+
`StaleAssignmentAuditor`.
|
|
260
|
+
<!-- pin: test/support/decision_parity/rules.rb#ending soon: the end-date cell, Home, the People filter and ending_within agree -->
|
|
261
|
+
- **`PermissionSet.in_hash_order`** (the order sets merge in), **`Operators::RuleColumn.names_for(model)`** (the
|
|
262
|
+
columns a rule may compare), **`FiltersResponseAttributes.unconfigured_strategy`** (one lookup for the response
|
|
263
|
+
filter and the attribute guard).
|
|
264
|
+
<!-- pin: test/permission_forge/release_readiness/one_definition_guard_test.rb#one definition: the order sets merge in (PermissionSet.in_hash_order) -->
|
|
265
|
+
- **`PermissionForge::SchemaRead`, the one reader of a model's columns** (`@api private`). A read that answers "no"
|
|
266
|
+
(not a column, no such table) reads the schema again once first — one metadata query, and `reset_column_information`
|
|
267
|
+
only when the database's columns differ from what Rails holds; a name confirmed missing costs nothing afterwards
|
|
268
|
+
until the table is read again, and a column that is there costs no query. Lists that build rows or reports, and the
|
|
269
|
+
save of a rule, compare with the database (`verify: true`). `SchemaRead.rereads` counts the re-reads.
|
|
270
|
+
<!-- pin: test/permission_forge/schema_freshness_test.rb#a name confirmed missing is not read again until the table is -->
|
|
271
|
+
- **Rule values are typed for PostgreSQL's `uuid`, `inet`, `cidr` and native enum columns, and for `json` / `jsonb`
|
|
272
|
+
/ array columns on every database** (`Operators::ValueCasters::PostgresqlTypes`, `ValueCasters::OPAQUE`). A `uuid`
|
|
273
|
+
column takes the text of a UUID in either case and in each notation PostgreSQL reads (RFC 4122 8-4-4-4-12, in
|
|
274
|
+
braces, after `urn:uuid:`, or the 32 digits without hyphens), normalised to RFC 4122 text; `inet` an IPv4 or IPv6
|
|
275
|
+
address IPAddr reads, with an optional prefix length (no netmask, no zone); `cidr` the same with no bits set right
|
|
276
|
+
of the prefix; a native enum (`CREATE TYPE … AS ENUM`) one of the type's labels exactly as the database has it, read
|
|
277
|
+
from `pg_enum` once per process and type and read again once before a label is called missing
|
|
278
|
+
(`SchemaRead.enum_label?`: a label `ALTER TYPE … ADD VALUE` added while the process runs); a model `enum` over a
|
|
279
|
+
native enum column maps its label first. A `json`, `jsonb` or array column takes no value under any built-in
|
|
280
|
+
operator but the null checks. `current_user` on a `uuid` column is the id where user ids are uuids. What this
|
|
281
|
+
changes for existing rules is under **Changed**.
|
|
282
|
+
<!-- pin: test/permission_forge/operators/value_typing_test.rb#a uuid column takes the text of a uuid, in any case and in each notation PostgreSQL reads, and nothing else -->
|
|
283
|
+
- **One schema per API answer.** `PermissionForge::Api::Schema` (`@api private`) declares the success envelopes
|
|
284
|
+
(`single`, `collection` with `meta.pagination`, `list`), the error envelope `{error: {status, messages}}` (field →
|
|
285
|
+
messages for 422, one sentence for 400, 401, 403, 404), the answers without a body, every serializer's keys and
|
|
286
|
+
types, and what each endpoint answers (`Schema::ENDPOINTS`). The numeric ids of other PermissionForge records are
|
|
287
|
+
marked `:foreign_identifier`; their value is unchanged.
|
|
288
|
+
<!-- pin: test/permission_forge/api/envelope_schema_test.rb#every serializer's keys are its Api::Schema payload, and every payload has its serializer -->
|
|
289
|
+
- **Every API route, in its success and every error class, is asserted against the schema**
|
|
290
|
+
(`api/envelope_schema_test.rb`, one test per verb and path, the catch-all included with `OPTIONS` and `TRACE`: 401
|
|
291
|
+
refused or unset, 404 API off or unknown record, 403 tenant, lambdas or lock, 400 for an unreadable JSON, form or
|
|
292
|
+
multipart body, 422), under the pseudo-locale `xx`.
|
|
293
|
+
<!-- pin: test/permission_forge/api/envelope_schema_test.rb#every API route has an Endpoint, and every Endpoint a route -->
|
|
294
|
+
<!-- pin: test/permission_forge/api/envelope_schema_test.rb#~answers the schema in its success and in every error class -->
|
|
295
|
+
- **The JSON samples of docs/admin-api.md are generated** by `bundle exec rake docs:api`
|
|
296
|
+
(`PermissionForge::Docs::ApiSamples`) from the schema and the serializers; `docs/api_samples_test.rb` fails while
|
|
297
|
+
the document is stale. The success samples show every key a permission set, a group and an audit event answer.
|
|
298
|
+
<!-- pin: test/permission_forge/docs/api_samples_test.rb#the json samples of docs/admin-api.md are what the schema and the serializers render -->
|
|
299
|
+
- **Rule builder: the preview is the saved rule's sentence, from the server.** A new `GET …/rules/preview` renders the
|
|
300
|
+
rule being built with the helper the Rules tab draws a saved rule with (`SentencesHelper#pf_rule_sentence`);
|
|
301
|
+
`pf_admin.js` fetches it as the value, "the current user", the list, the range or "or empty" change, instead of
|
|
302
|
+
rebuilding a value token in JavaScript (ticking "the current user" left the preview at "Only show Customers where
|
|
303
|
+
Owner is …." until the rule was saved). Without JavaScript the preview changes on the builder's refresh, and the
|
|
304
|
+
URL is a page of its own.
|
|
305
|
+
<!-- pin: test/permission_forge/html/rule_builder_explanations_test.rb#the preview is the sentence the saved rule shows, one string from one helper -->
|
|
306
|
+
<!-- pin: test/permission_forge/html/admin_behaviour_test.rb#the rule preview names the current user once it is ticked, without a doubled period -->
|
|
307
|
+
<!-- pin: test/permission_forge/html/admin_behaviour_test.rb#pf_admin.js fetches the preview sentence as the value inputs change -->
|
|
308
|
+
- **`Operators::RuleProblem::REMEDIES` / `#remedy`** — what the admin changes to correct a rule that matches nothing,
|
|
309
|
+
by kind (`:join`, `:path`, `:operator`, `:field`, `:value`); the taxonomy guard fails for a kind without one.
|
|
310
|
+
<!-- pin: test/permission_forge/html/rule_builder_explanations_test.rb#the banner names the fix for each kind of problem, in every locale -->
|
|
311
|
+
<!-- pin: test/permission_forge/operators/rule_problem_taxonomy_test.rb#every kind has a remedy, and every remedy is a kind with a fix sentence in every locale -->
|
|
312
|
+
- **Labels of stored names have one definition.** `Admin::FieldLabel` (a column, field or association name),
|
|
313
|
+
`Admin::ModelLabel` (a model or Resource, and its plural) and `Admin::ActionLabel` (an action key) answer the host's
|
|
314
|
+
own translation when it has one (`activerecord.attributes.*` / `activerecord.models.*`, through
|
|
315
|
+
`human_attribute_name` / `model_name.human` with a sentinel default — Rails' humanized English fallback is not a
|
|
316
|
+
translation), else the row's label, else the name verbatim, which the admin draws in `<code>`.
|
|
317
|
+
`Admin::ErrorSentence` says a validation sentence from the gem's keys (`permission_forge.validations.attributes.*`,
|
|
318
|
+
`full_message`), `Admin::Prefill` what a new-record form shows. Helpers `pf_field_label`, `pf_field_label_text`,
|
|
319
|
+
`pf_action_label_text`; `pf_resource_label` / `pf_resource_plural` read `ModelLabel`.
|
|
320
|
+
<!-- pin: test/permission_forge/admin/labels_test.rb#a field is labelled by the host translation, else the row label, else the name verbatim -->
|
|
321
|
+
- **`Admin::StateLabel`** (the one registry of the admin's state words and glyphs, `ui.states.<kind>.<state>`) and
|
|
322
|
+
**`Admin::ActorLabel`** (how the admin names who made a change): internal.
|
|
323
|
+
<!-- pin: test/permission_forge/html/state_rendering_test.rb#the Effective access legend words the states as its cells do -->
|
|
324
|
+
- **`PermissionForge::ParamShapes#pf_integer_param(key, default:, range:)`** — the one reader of a whole-number
|
|
325
|
+
request parameter (a page, a page size, a count): the number when the value is one inside the range (digits or a
|
|
326
|
+
JSON number), the default for every other shape (text, `2abc`, fractions, lists, objects, blanks, 0 and negatives
|
|
327
|
+
below the range). The API's pagination reads `page` and `per_page` through it (see **Changed**), and so do the audit
|
|
328
|
+
log's and the person list's page, the audit tab's `days` and the rule builder's `remove_step` (`ParamShapes.integer`);
|
|
329
|
+
a static guard refuses `params[...].to_i` in the admin and the API.
|
|
330
|
+
<!-- pin: test/permission_forge/controllers/param_shapes_test.rb#anything else is the default: 0 and negatives below the range, text, fractions, lists, objects, blanks -->
|
|
331
|
+
- **Every code sample in README.md and docs/ says what it is**: its fence carries `run` (a console sample the
|
|
332
|
+
documentation app executes, `# => literal` asserted), `file=<path>` (a complete file written into that app) or
|
|
333
|
+
`fragment`. A ruby block that rescues a denial error is held to the handler that works in a Rails 8 Turbo app.
|
|
334
|
+
<!-- pin: test/permission_forge/docs/doc_blocks_test.rb#every ruby, erb and bash block in README.md and docs/ carries a marker and follows the rules -->
|
|
335
|
+
- **The documentation names only what exists** (`docs/references_test.rb`): links and anchors (GitHub links to
|
|
336
|
+
this repository too, also those the code prints), options (`config.<name>`, `name = value`, a bare name shaped like
|
|
337
|
+
one), `permission_forge:<task>` names (each documented with the environment variables it reads, `ACTOR` / `REASON`
|
|
338
|
+
included), `PermissionForge::…` constants, and the defaults a sentence states.
|
|
339
|
+
<!-- pin: test/permission_forge/docs/references_test.rb#every defined rake task is documented, with the environment variables it reads -->
|
|
340
|
+
<!-- pin: test/permission_forge/docs/references_test.rb#every link to this repository the code prints names a file and anchor of it -->
|
|
341
|
+
<!-- pin: test/permission_forge/docs/references_test.rb#every option the docs write as `name = value`, or as a bare `name` shaped like an option, is one -->
|
|
342
|
+
- **`bundle exec rake docs:configuration`** regenerates the Option reference of `docs/configuration.md` from the code
|
|
343
|
+
(the Settings catalog's sections and order, `Configuration.new`'s defaults, `StrategyValues`' values; every public
|
|
344
|
+
option has an entry); the suite fails while it is stale. Gem development only.
|
|
345
|
+
<!-- pin: test/permission_forge/docs/configuration_reference_test.rb#the Option reference in docs/configuration.md is what the code generates -->
|
|
346
|
+
- **The admin's layout is measured in a browser**: `test/browser/layout_probe.js` renders every page, error state and
|
|
347
|
+
flow of `test/support/preview_app.rb` at phone and desktop widths in en, sq and mk under Chrome (the `browser` CI
|
|
348
|
+
job, non-required until green for a release); without a browser, `html/layout_lint_test.rb` checks placeholders,
|
|
349
|
+
wide grids and scroll containers, and `html/admin_page_paths_test.rb` keeps the screen list complete (every GET
|
|
350
|
+
route, every `422` render site).
|
|
351
|
+
<!-- pin: test/permission_forge/html/admin_page_paths_test.rb#every GET route of the admin is reached by a screen -->
|
|
352
|
+
- **Scroll frames** (`pf_scroll_frame`, `.pf-scroll-x`) are regions a keyboard reaches (`tabindex="0"`, named for
|
|
353
|
+
assistive technology) with shadows at the edges that show when there is more to the side.
|
|
354
|
+
<!-- pin: test/permission_forge/html/layout_lint_test.rb#every wide grid is in a scroll frame that is reachable and named, on every screen -->
|
|
355
|
+
- **The suite proves the gem owns every key it renders.** The dummy app runs without `rails-i18n`, and
|
|
356
|
+
`html/locale_leak_test.rb` renders every screen, re-rendered form, toast, CSV export and API error class under a
|
|
357
|
+
pseudo-locale (`test/support/pseudo_locale.rb`): a word that is neither a translation nor host data fails.
|
|
358
|
+
<!-- pin: test/permission_forge/html/locale_leak_test.rb#every screen shows only translated words and host data -->
|
|
359
|
+
- **Static lints for the admin's words.** `release_readiness/inflection_guard_test.rb`: no English inflection
|
|
360
|
+
(`humanize`, `pluralize`, `to_sentence`, `full_messages`, `time_ago_in_words`, ...) and no Rails-core key family in
|
|
361
|
+
`app/` or `lib/permission_forge/`; `i18n_test.rb`: a sq/mk value identical to English needs a reason, one pair
|
|
362
|
+
of quotation marks per locale, one label per state; `release_readiness/column_default_prefill_test.rb`: no form
|
|
363
|
+
pre-fills a string column default; `AdminPagesTest` lints every re-rendered form as it does every screen.
|
|
364
|
+
<!-- pin: test/permission_forge/release_readiness/inflection_guard_test.rb#nothing in app/ or lib/permission_forge inflects English -->
|
|
365
|
+
<!-- pin: test/permission_forge/release_readiness/column_default_prefill_test.rb#no new-record screen or re-rendered form pre-fills a text default -->
|
|
366
|
+
<!-- pin: test/permission_forge/html/admin_pages_test.rb#markup is CSP-safe and namespaced, on every screen and every re-rendered form -->
|
|
367
|
+
- **Release-readiness guards.** `stored_data_fail_closed_test.rb` poisons every interpreted column of the eight models
|
|
368
|
+
past validation (nothing raises or widens, the audit names the row); `one_definition_guard_test.rb` (G4) fails a
|
|
369
|
+
re-derivation of a one-definition rule (`test/support/one_definition/*.rb`); `decision_parity_test.rb` (G5) checks
|
|
370
|
+
that two surfaces answering one question agree; `schema_freshness_test.rb` (G6) alters a table while the suite runs;
|
|
371
|
+
`services/rule_blame_property_test.rb` (G7) checks a denial's blame on generated cases; `changelog_pins_test.rb`
|
|
372
|
+
ties every CHANGELOG bullet to its test.
|
|
373
|
+
<!-- pin: test/permission_forge/release_readiness/stored_data_fail_closed_test.rb#PermissionSet: every interpreted column, poisoned past validation, never raises or widens -->
|
|
374
|
+
<!-- pin: test/permission_forge/release_readiness/one_definition_guard_test.rb#a re-derivation is a hit (negative fixture) -->
|
|
375
|
+
<!-- pin: test/permission_forge/schema_freshness_test.rb#every reader sees a column a migration added after the schema was read, without a reset -->
|
|
376
|
+
<!-- pin: test/permission_forge/release_readiness/changelog_pins_test.rb#the guard reports what it should, on a fixture -->
|
|
377
|
+
- **Host model shapes, Rails' reader surface and hostile parameters are covered end to end.** `host_shapes_test.rb`
|
|
378
|
+
runs every enforcement path on a namespaced model with a uuid primary key (`Billing::Invoice`) and one with
|
|
379
|
+
`encrypts` and an `enum` (`PfEncryptedNote`); `concerns/masked_reader_surface_test.rb` calls every attribute-method
|
|
380
|
+
pattern of the Rails in use on a hidden column (one that hands out the stored value must be a raw path
|
|
381
|
+
`docs/masked-attributes.md` lists); `security/route_fuzz_test.rb` says what each hostile parameter must be read as
|
|
382
|
+
and plays the uniqueness race; a canary watches the `default:` option of `human_attribute_name` / `ModelName#human`.
|
|
383
|
+
<!-- pin: test/permission_forge/host_shapes_test.rb#Billing::Invoice: permission_scoped lists the rows the rule allows and no other -->
|
|
384
|
+
<!-- pin: test/permission_forge/concerns/masked_reader_surface_test.rb#every pattern that returns the stored value of a hidden column is a documented raw path, and every documented raw path still does -->
|
|
385
|
+
<!-- pin: test/permission_forge/security/route_fuzz_test.rb#~reads every shape of -->
|
|
386
|
+
<!-- pin: test/permission_forge/rails_internals_canary_test.rb#human_attribute_name / ModelName#human: the default: option is answered when the host has no translation, the host's translation when it has one -->
|
|
387
|
+
- **`test/rls/rls_rule_parity_test.rb` has a case for every `Operators::RuleProblem` kind**: each compiles to `FALSE`,
|
|
388
|
+
selects nothing in the app layer and hides every row under a real policy; the matrix covers `uuid`, `inet`, `cidr`,
|
|
389
|
+
native enum, `jsonb` and `text[]` columns.
|
|
390
|
+
<!-- pin: test/rls/rls_rule_parity_test.rb#every kind of rule the app layer matches nothing for has a parity case -->
|
|
391
|
+
- **`config.no_read_access_strategy`: record reads that need read access.** `:rules_only` (the default) keeps the
|
|
392
|
+
record reads as they were: `find_by`, `find_by!`, `permission_scoped` and the `auto_query_scoping` default scope
|
|
393
|
+
apply the rules of the user's entries, so a signed-in user with no entry for the model, or one without
|
|
394
|
+
`read_access`, has no rules there and gets every row. `:deny` refuses them whole: for a signed-in user whose
|
|
395
|
+
entries along the model's Resource chain do not all grant `read_access` (whatever took it — no grant, a Muting
|
|
396
|
+
Permission Set, a Resource renamed off its model, a granting set whose conditions cannot be judged),
|
|
397
|
+
`permission_scoped` and the default scope are `none`, and `find_by` / `find_by!` raise `NotAuthorizedError` for an
|
|
398
|
+
existing record with one `failure_messages` entry from the locale (`permission_forge.denials.no_read_access`; en,
|
|
399
|
+
sq, mk), `nil` / `RecordNotFound` for a missing one; each is an `access.denied` with reason `no_read_access`. View
|
|
400
|
+
All and Modify All never stand in for read; with nobody signed in `nil_user_strategy` decides. One definition,
|
|
401
|
+
`PermissionForge::RecordReadAccess`. A README checklist row, a Settings page row and a strategy value checked when
|
|
402
|
+
set. The PostgreSQL RLS policies follow it: under `:deny` the SELECT / UPDATE / DELETE policies show a session user
|
|
403
|
+
no row without read access to the table's Resource chain, read per statement as `ResourceChain.readable?` reads it
|
|
404
|
+
(an active, unexpired assignment to an active granting set with an active Resource row for the model, every entry
|
|
405
|
+
along the STI chain granting `read_access`, no Muting Permission Set taking it; a conditional set never grants read
|
|
406
|
+
there, a conditional muting set always takes it — narrower, never wider); under `:rules_only` the policy is
|
|
407
|
+
unchanged. The strategy is part of the policy digest, so `rls:status` reports `STALE` after it is switched until
|
|
408
|
+
the next `rls:sync`. Part of `GENERATOR_VERSION` 7 (new in this release, so no further bump).
|
|
409
|
+
<!-- pin: test/permission_forge/concerns/no_read_access_strategy_test.rb#no entry, :deny: no rows, find_by raises with the locale message, a missing record is still missing -->
|
|
410
|
+
<!-- pin: test/permission_forge/concerns/no_read_access_strategy_test.rb#no entry, :rules_only: every read returns every row, as before -->
|
|
411
|
+
<!-- pin: test/permission_forge/concerns/no_read_access_strategy_test.rb#STI: an unreadable base denies the subclass that grants read itself -->
|
|
412
|
+
<!-- pin: test/permission_forge/release_readiness/stored_data_fail_closed_test.rb#~under no_read_access_strategy :deny, no poisoning widens a record read -->
|
|
413
|
+
<!-- pin: test/rls/rls_read_access_parity_test.rb#a user with no entry for the model: every row under :rules_only, none under :deny, in both layers -->
|
|
414
|
+
<!-- pin: test/rls/rls_read_access_parity_test.rb#read removed by a Muting Permission Set: none under :deny, in both layers -->
|
|
415
|
+
<!-- pin: test/rls/rls_read_access_parity_test.rb#STI: a subclass entry without read hides its rows (and its subclasses) under :deny -->
|
|
416
|
+
<!-- pin: test/permission_forge/services/rls_policy_generator_test.rb#the digest follows no_read_access_strategy, so rls:status reports the policy stale after it changes -->
|
|
417
|
+
|
|
418
|
+
- **`cache.settled`: what the cache store said after an erasure or a consent withdrawal.** `erasure.performed` and
|
|
419
|
+
`consent.withdrawn` are written in the operation's transaction, before its cache work, so they record
|
|
420
|
+
`cache_confirmed: nil`. A `cache.settled` event (category `system`, `subject_user_id`, `data.operation` `erasure` or
|
|
421
|
+
`consent_withdrawal`, and the `Cache::Outcome`) now follows on the same `correlation_id` with the store's answer;
|
|
422
|
+
inside a caller's transaction the work runs again after its commit, measured, and the event is written then (never
|
|
423
|
+
after a rollback). A failed post-commit invalidation of an erased person leaves this trace
|
|
424
|
+
(`cache_confirmed: false`), and its sentence says the store did not confirm it (en/sq/mk).
|
|
425
|
+
<!-- pin: test/permission_forge/services/cache_settlement_outcome_test.rb#an erasure records what the store said in cache.settled, on the erasure correlation id -->
|
|
426
|
+
<!-- pin: test/permission_forge/services/cache_settlement_outcome_test.rb#inside a caller transaction cache.settled is written after the commit, with the outcome then -->
|
|
427
|
+
<!-- pin: test/permission_forge/services/cache_settlement_outcome_test.rb#a rolled-back erasure settles nothing -->
|
|
428
|
+
- **The route fuzz poisons each parameter alone, inside a valid request.** `security/route_fuzz_test.rb` poisoned
|
|
429
|
+
every parameter at once, so a parameter read only when another is valid (`days` behind `tab=audit`,
|
|
430
|
+
`rule[remove_step]` behind a rule form) was never reached. Per route it now poisons each parameter the controller
|
|
431
|
+
reads (found in the source by `FuzzParams`: `params[...]`, the `pf_*_param` readers, permitted keys) one at a time
|
|
432
|
+
inside each valid request of the route (`valid_contexts`), then each attribute of each attribute section, the API
|
|
433
|
+
also as a JSON body with `null`; a guard fails when a parameter the engine reads is missing from the fuzz table.
|
|
434
|
+
<!-- pin: test/permission_forge/security/route_fuzz_test.rb#every parameter the engine reads is in the fuzz table -->
|
|
435
|
+
<!-- pin: test/permission_forge/security/route_fuzz_test.rb#every attribute section the engine reads has a valid request that reaches it -->
|
|
436
|
+
<!-- pin: test/permission_forge/security/route_fuzz_test.rb#~answers each read parameter alone below 500 -->
|
|
437
|
+
- **Static guards: the cache report per method, integer parameters, warning severities.** The cache-report guard
|
|
438
|
+
places each invalidation per method (a `Cache::Report` block, `cache_outcome`, or a method all of whose callers are
|
|
439
|
+
measured) instead of exempting a whole file once it mentions `Cache::Report`, and also sees
|
|
440
|
+
`regenerate_permissions`, `cache.delete` / `expunge_many` / `delete_matched` and `lib/permission_forge/admin`; a new
|
|
441
|
+
guard refuses `params[...].to_i` and `.to_s.to_i` in the admin and the API; every `Warnings.emit` key has its
|
|
442
|
+
severity, and a reason when it is not SECURITY, in the warnings channel test, whose needle also sees a logger's
|
|
443
|
+
`error` / `add` / `fatal`.
|
|
444
|
+
<!-- pin: test/permission_forge/release_readiness/cache_report_guard_test.rb#negative fixture: an unmeasured method in a file that measures elsewhere is a hit -->
|
|
445
|
+
<!-- pin: test/permission_forge/release_readiness/integer_param_guard_test.rb#no request value is read with to_i -->
|
|
446
|
+
<!-- pin: test/permission_forge/boot_warnings_channel_test.rb#every warning has its severity in SEVERITY, the one its site passes, and a reason when it is config -->
|
|
447
|
+
- **API: every reference to another record is answered by its uuid too.** Under `api_record_identifier = :uuid` the
|
|
448
|
+
answers named the records they refer to by numeric id only — a Permission Set said `permission_set_group_id: 4` right
|
|
449
|
+
after the PATCH that sent the group's uuid — so a client that only knows uuids could not follow them (field test
|
|
450
|
+
round 8, D#29). Each such key is now followed by the record's uuid: `permission_set_group_uuid` (a Permission Set, a
|
|
451
|
+
group member), `permission_set_uuid` (a Resource, a rule, an assignment) and `resource_uuid` (a field, an action, a
|
|
452
|
+
rule), under either identifier setting; the `_id` keys keep their value and type. One declaration in the serializer
|
|
453
|
+
layer (`Serializers::BaseSerializer.reference`), one query per reference for a page of records; `Api::Schema` types
|
|
454
|
+
the new keys `:foreign_uuid`. The `_uuid` keys are answer-only: send the `_id` key to change a reference (it takes
|
|
455
|
+
the uuid under `:uuid`, as before); a `_uuid` key sent back with a whole answer is ignored like any key the endpoint
|
|
456
|
+
does not take, so such a PATCH stays idempotent and a stale copy cannot undo another admin's move.
|
|
457
|
+
`user_id` (your user's key), a serializer of your own (`api_serializer_overrides`) and an AMS serializer are
|
|
458
|
+
unchanged.
|
|
459
|
+
<!-- pin: test/permission_forge/api/reference_uuids_api_test.rb#a set moved into a group by its uuid answers the group by its uuid too -->
|
|
460
|
+
<!-- pin: test/permission_forge/api/reference_uuids_api_test.rb#every answer names the set, the Resource and the group it refers to by their uuids -->
|
|
461
|
+
<!-- pin: test/permission_forge/api/reference_uuids_api_test.rb#a page looks the uuids up in one query per reference, however many rows it has -->
|
|
462
|
+
<!-- pin: test/permission_forge/api/reference_uuids_api_test.rb#an answer sent back whole moves the set as its id key says, the same every time -->
|
|
463
|
+
<!-- pin: test/permission_forge/api/reference_uuids_api_test.rb#a body with only permission_set_group_uuid changes nothing: the companion is answer-only -->
|
|
464
|
+
<!-- pin: test/permission_forge/api/envelope_schema_test.rb#every foreign identifier is followed by the uuid of the same record, null where it may be null -->
|
|
465
|
+
- **`masked_attributes_nil_user_strategy = :hide_in_requests`: masked attributes are hidden in a web request with
|
|
466
|
+
nobody signed in, and keep their stored values in jobs, rake, the runner and the console** (GitHub #11). Under
|
|
467
|
+
`:bypass` a public page, a webhook, a controller that forgot authentication or a resolver that returns nil got every
|
|
468
|
+
guarded attribute as stored, and nothing recorded it. The new value hides them there exactly as `:hide` does (the
|
|
469
|
+
primary key and the raw paths untouched, the user model exempt for sign-in). "A web request" has one definition,
|
|
470
|
+
`PermissionForge::WebRequest`: the engine puts `WebRequest::Middleware` at the top of the host's middleware stack
|
|
471
|
+
(`config.app_middleware`; a middleware inserted above it with `insert_before 0`, as rack-cors documents, reads
|
|
472
|
+
outside the request: the boot log names it, `web_request_middleware_above_hide` / `_record`, and
|
|
473
|
+
`rake permission_forge:audit` lists it, `middleware_above_web_request`, info), which marks the request in `ExecutionState` slot `:pf_web_request` (execution scope, kept
|
|
474
|
+
at the end of an executor run) until the response is finished (`rack.response_finished`, else when the body is
|
|
475
|
+
closed; at once when the app raises) — so a middleware or a `before_action` that reads a model before sign-in counts,
|
|
476
|
+
as do a job run with `perform_now` and a mail sent with `deliver_now` during the request; jobs run by a worker,
|
|
477
|
+
Action Cable channel actions, rake, the runner, the console and `ActionController::TestCase` tests are outside.
|
|
478
|
+
`Configuration#masked_attributes_without_user_for(klass)` (`:hide`, `:record` or `:bypass`) is the one decision
|
|
479
|
+
behind `hide_masked_attributes_without_user_for?`; `hide_masked_attributes_without_user_in_requests?` says whether
|
|
480
|
+
the strategy hides at least in requests. `MaskedAttributes.explain` adds `in_web_request`. The Settings page lists
|
|
481
|
+
the three values with what each does (en, sq, mk). A record that read an attribute as hidden because nobody was
|
|
482
|
+
signed in refuses a later write to that attribute as a masked write-back (`masked_write_protection`: `:skip` keeps
|
|
483
|
+
the stored value, `:raise` raises; `write.masked_skipped` `detected_by: hidden_without_user`), so a webhook that
|
|
484
|
+
normalizes `name` from its hidden value does not store `""`. Only an attribute that held a value when it was read
|
|
485
|
+
counts (an empty column or an unfilled attribute of a new record takes a default or a fill-if-blank; a value
|
|
486
|
+
submitted on a new record is kept from a normalizer or a default that reads it), and the attributes are forgotten
|
|
487
|
+
on `reload`, not after a save. A deliberate overwrite of a value nobody could see (a change-email link
|
|
488
|
+
with nobody signed in) is refused too, and logged once per attribute and process; a value derived for another
|
|
489
|
+
attribute (`domain` from a hidden `email`) cannot be told apart: sign a system user in for such flows and for
|
|
490
|
+
webhooks that normalize data.
|
|
491
|
+
<!-- pin: test/permission_forge/concerns/masked_attributes_in_requests_test.rb#a value worked out from an attribute read as hidden with nobody signed in is not saved over it -->
|
|
492
|
+
<!-- pin: test/permission_forge/concerns/masked_attributes_in_requests_test.rb#under masked_write_protection :raise such a write raises, and an attribute never read is written -->
|
|
493
|
+
<!-- pin: test/permission_forge/concerns/masked_attributes_in_requests_test.rb#nothing real was hidden: a new record and an empty column take a default or a fill-if-blank -->
|
|
494
|
+
<!-- pin: test/permission_forge/concerns/masked_attributes_in_requests_test.rb#a stored value read hidden is kept; overwriting it without a sign-in is refused, recorded and logged once -->
|
|
495
|
+
<!-- pin: test/permission_forge/concerns/masked_attributes_in_requests_test.rb#the hidden reads are forgotten on reload, not after a save -->
|
|
496
|
+
<!-- pin: test/permission_forge/concerns/masked_attributes_in_requests_test.rb#a value submitted on a new record is kept from a normalizer or a default that reads it hidden -->
|
|
497
|
+
<!-- pin: test/permission_forge/web_request_test.rb#the boot check warns while the strategy depends on the request, naming what runs above -->
|
|
498
|
+
<!-- pin: test/permission_forge/services/security_audit_test.rb#middlewares inserted above the request marker are an info, unless the strategy is :hide -->
|
|
499
|
+
<!-- pin: test/permission_forge/concerns/masked_attributes_in_requests_test.rb#:hide_in_requests hides every guarded attribute in a web request with nobody signed in, before_action included -->
|
|
500
|
+
<!-- pin: test/permission_forge/concerns/masked_attributes_in_requests_test.rb#:hide_in_requests hides a read in a middleware below WebRequest::Middleware, before any controller -->
|
|
501
|
+
<!-- pin: test/permission_forge/concerns/masked_attributes_in_requests_test.rb#:hide_in_requests keeps the stored values outside a web request: a job, the console, a runner, rake -->
|
|
502
|
+
<!-- pin: test/permission_forge/web_request_test.rb#the engine puts the middleware at the top of the host application's stack -->
|
|
503
|
+
<!-- pin: test/permission_forge/web_request_test.rb#below the middleware the request is a web request, with its env, until the server closes the body -->
|
|
504
|
+
<!-- pin: test/permission_forge/web_request_test.rb#a run completed inside the request does not end it -->
|
|
505
|
+
<!-- pin: test/permission_forge/html/masked_nil_user_setting_test.rb#the Settings page lists bypass, hide_in_requests and hide, marks the current one and explains them in en, sq and mk -->
|
|
506
|
+
- **Under `:bypass`, a masked value returned in a web request with nobody signed in is recorded** (GitHub #11): the
|
|
507
|
+
runtime audit event `field.unmasked_without_user` (group `:bypass`: published always, stored when
|
|
508
|
+
`audit_runtime_events` includes `:bypass`) once per request per model and attribute — the model, the attribute, the
|
|
509
|
+
request's method, path and endpoint, and its request id and IP as columns; never a value — for exactly the reads
|
|
510
|
+
`:hide_in_requests` would hide; and one SECURITY warning per model and process in the log, naming the model, the
|
|
511
|
+
attribute and the request (models by name; an `inspect` records nothing). With nobody listening and the group not
|
|
512
|
+
stored, nothing is built or allocated: once the model has been warned about, `attributes` returns the stored hash
|
|
513
|
+
as outside a request and a read pays a few lookups (in the gem's benchmark, 1,000 records of 8 columns: getters
|
|
514
|
+
about 20% and `as_json` about 17% slower inside a request than in 0.14, about 5% outside one). Sentence in en,
|
|
515
|
+
sq and mk.
|
|
516
|
+
<!-- pin: test/permission_forge/concerns/masked_attributes_in_requests_test.rb#:bypass returns the stored values in a web request and publishes field.unmasked_without_user once per request per attribute -->
|
|
517
|
+
<!-- pin: test/permission_forge/concerns/masked_attributes_in_requests_test.rb#field.unmasked_without_user is stored with the bypass group, never with a value -->
|
|
518
|
+
<!-- pin: test/permission_forge/concerns/masked_attributes_in_requests_test.rb#a SECURITY warning names the model, the attribute and the request, once per model and process -->
|
|
519
|
+
<!-- pin: test/permission_forge/concerns/masked_attributes_in_requests_test.rb#with nobody listening and the bypass group not stored, a read in a request builds nothing and allocates no more -->
|
|
520
|
+
<!-- pin: test/permission_forge/concerns/masked_attributes_in_requests_test.rb#inspect in a request publishes nothing and warns nothing: a debugging read -->
|
|
521
|
+
|
|
522
|
+
### Changed
|
|
523
|
+
|
|
524
|
+
- **`config.preset = :strict` sets `masked_attributes_nil_user_strategy = :hide_in_requests`, and new installs get it
|
|
525
|
+
from the generator** (it was in no preset, `Presets::OPT_IN`, which is now empty). Under the strict preset, or in a
|
|
526
|
+
new install, a page that reads a masked model before anyone signs in (a tenant or subdomain lookup, a public sign-up
|
|
527
|
+
page, a mail sent with `deliver_now`) reads hidden values; jobs and the console keep the stored values. `:hide`
|
|
528
|
+
counts as closed (no `strict_overrides`). The library default stays `:bypass`. `rake permission_forge:audit` reports
|
|
529
|
+
`:bypass` (`masked_nil_user_bypass`, medium) with `:hide_in_requests` as the remedy and its trade-off, and
|
|
530
|
+
`:hide_in_requests` as `masked_nil_user_outside_requests` (info); `user_model_unmasked_without_user` covers both
|
|
531
|
+
hiding values. **Opt out:** `config.masked_attributes_nil_user_strategy = :bypass`.
|
|
532
|
+
<!-- pin: test/permission_forge/configuration_presets_test.rb#:strict sets masking with nobody signed in to :hide_in_requests; :hide counts as closed -->
|
|
533
|
+
<!-- pin: test/permission_forge/configuration_presets_test.rb#a new install gets the safe subset explicitly and :strict offered, not active -->
|
|
534
|
+
<!-- pin: test/permission_forge/services/security_audit_test.rb#masking with nobody signed in: :bypass is medium and recommends :hide_in_requests, which leaves an info -->
|
|
535
|
+
<!-- pin: test/permission_forge/install/fresh_install_test.rb#a default install on integer user keys works end to end -->
|
|
536
|
+
|
|
537
|
+
- **A rule whose field name is not a column of its model does not save.** `PermissionSetRule` adds "names
|
|
538
|
+
`onwer_id`, which is not a column of Customer" on `field_name` (en, sq, mk; it names the stored value, like the path
|
|
539
|
+
message) when the Resource's model loads and the name is no column of its table — along `field_accessor_path` for a
|
|
540
|
+
join rule — so the JSON API answers its field-keyed 422 instead of 201, and the admin shows the message. Checked for
|
|
541
|
+
a new rule and when the field name, path, join type or resource changes, so an existing row stays editable in its
|
|
542
|
+
other attributes (a stored `'filter'` join type and the admin's nil are one); saving compares the table's columns
|
|
543
|
+
with the database (`RuleColumn.for_rule(rule, verify: true)`, one metadata query per model along the path), so a
|
|
544
|
+
column a migration just added is accepted; skipped when the model cannot be loaded, its table does not exist or the
|
|
545
|
+
path cannot be followed. Copying a Permission Set (or a group) copies such a rule as it is: leaving it out would
|
|
546
|
+
widen the copy. **No opt-out:** the rule could only ever raise (and now matches nothing); write it past the model if
|
|
547
|
+
you must.
|
|
548
|
+
<!-- pin: test/permission_forge/models/permission_set_rule_column_test.rb#a field name that is no column of the model is refused on field_name -->
|
|
549
|
+
<!-- pin: test/permission_forge/api/permission_set_rules_controller_test.rb#POST create answers 422 on field_name for a column the model does not have -->
|
|
550
|
+
<!-- pin: test/permission_forge/models/permission_set_rule_column_test.rb#an existing row is checked only when what it names changes -->
|
|
551
|
+
<!-- pin: test/permission_forge/models/permission_set_rule_column_test.rb#a column the process confirmed missing before a migration added it is accepted at save -->
|
|
552
|
+
<!-- pin: test/support/decision_parity/rules.rb#the column-typo validation message names the stored value, like the path message -->
|
|
553
|
+
- **A join rule whose association path cannot be joined does not save.** `PermissionSetRule` adds "names ownr, which
|
|
554
|
+
is not an association of Customer" on `field_accessor_path`, or why no join can be built through it (a polymorphic
|
|
555
|
+
association, a class that cannot be loaded, a scope that takes the record — also along a `:through` —, a `:through`
|
|
556
|
+
/ `:source` / `inverse_of` naming nothing, an empty step in `parent..owner`; en, sq, mk;
|
|
557
|
+
`Operators::RuleProblem#cause` / `#key`), so the JSON API answers its field-keyed 422 instead of 201. Checked for a
|
|
558
|
+
new join rule and when its path, join type or resource changes (an existing row stays editable in its other
|
|
559
|
+
attributes); a filter rule's path is never checked. Every association ActiveRecord joins is accepted
|
|
560
|
+
(`Operators::RulePath` asks what `Associations::JoinDependency#build` asks; a canary watches it). Copying a
|
|
561
|
+
Permission Set copies such a rule as it is. **No opt-out:** the rule could only ever raise (and now matches
|
|
562
|
+
nothing).
|
|
563
|
+
<!-- pin: test/permission_forge/models/permission_set_rule_path_test.rb#each cause has its own message -->
|
|
564
|
+
<!-- pin: test/permission_forge/api/permission_set_rules_controller_test.rb#POST create answers 422 on field_accessor_path for an association the model does not have -->
|
|
565
|
+
<!-- pin: test/permission_forge/models/permission_set_rule_path_test.rb#a path ActiveRecord cannot join is refused with its cause, and one it joins is accepted -->
|
|
566
|
+
- **A rule qualified with a name its query gives no table matches nothing, and is refused for a new or changed
|
|
567
|
+
rule:** `nosuch.name`, the typo `customer.name`, a column of a join's ON clause (`supplier_id.id` on a rule joining
|
|
568
|
+
`supplier`) and, on a filter rule, another model's table (`suppliers.name`: a filter rule compares its own table).
|
|
569
|
+
They reached SQL as written and raised for every holder, also under `auto_query_scoping`. The audit
|
|
570
|
+
(`missing_rule_columns`, `foreign_table_filter_rules`) and Home list stored ones. A join rule may still name a table
|
|
571
|
+
its query joins itself or an alias its join gives (`parents_customers`). **Opt out:** use a join rule (`join_type` +
|
|
572
|
+
`field_accessor_path`) for a column of another table.
|
|
573
|
+
<!-- pin: test/permission_forge/models/permission_set_rule_column_test.rb#a qualifier naming no table is refused; a join rule may name a table or an alias its query gives -->
|
|
574
|
+
<!-- pin: test/permission_forge/services/permission_rule_applier_test.rb#a filter rule qualified with a table of another model matches nothing in every gem-built query -->
|
|
575
|
+
<!-- pin: test/permission_forge/services/permission_rule_applier_test.rb#a join rule qualified with a name the ON clause mentions, which is no table or alias, matches nothing -->
|
|
576
|
+
- **A rule value its column cannot hold is refused on save** (`field_value`: "is not a value Customer.supplier_id can
|
|
577
|
+
hold"; the JSON API answers 422), for a new rule and when its value, operator, field name, path, join type or
|
|
578
|
+
resource changes — `'5_id'` on an integer column, `'maybe'` on a boolean one, `'banana'` on a `uuid` one, a
|
|
579
|
+
`between` bound or a list element the column cannot hold. Such a rule matched nothing for everyone it applies to. A
|
|
580
|
+
rule saved before (or written past the model) stays editable in its other attributes, still matches nothing, and is
|
|
581
|
+
still listed by the audit (`unusable_rule_values`) and on Home; a copy keeps its original's value. `current_user` is
|
|
582
|
+
refused only where no id could be held (a boolean, date, address, json or enum column). **No opt-out:** the rule
|
|
583
|
+
could only ever deny every record to its holders; saving it hid a lock-out behind a successful save.
|
|
584
|
+
<!-- pin: test/permission_forge/operators/rule_problem_taxonomy_test.rb#unusable_value: a new rule is refused with the problem's message -->
|
|
585
|
+
- **What a rule or field row names is checked when it changes.** `operator` and `join_type` (`PermissionSetRule`) and
|
|
586
|
+
`mask_strategy` (`ResourceField`, also when `mask_access` changes) are validated for a new row and when the
|
|
587
|
+
attribute changes — as `field_name` and the association path are — so a row written before the check, or past the
|
|
588
|
+
model (a rolling deploy, another application on the shared tables, a custom mask strategy since removed from the
|
|
589
|
+
configuration), stays editable in its other attributes and can be switched off or renamed through the admin and the
|
|
590
|
+
API. It matches nothing (a rule) or masks fully (a field) until it is fixed; the audit and the admin name it.
|
|
591
|
+
**No opt-out:** a loosening (such rows could not be edited at all).
|
|
592
|
+
<!-- pin: test/permission_forge/operators/rule_problem_taxonomy_test.rb#unknown_operator: a row written before the check stays editable in its other attributes -->
|
|
593
|
+
- **A rule whose field name is not a name** (a trailing or doubled dot) is listed by the audit under
|
|
594
|
+
`missing_rule_columns` (High while held) instead of `unusable_rule_values`, and on Home. **No opt-out:** a report
|
|
595
|
+
classification; the rule matched nothing before and after.
|
|
596
|
+
<!-- pin: test/permission_forge/services/security_audit_test.rb#rules whose value the column cannot hold are reported as matching nothing -->
|
|
597
|
+
- **The admin's rule builder refuses a posted condition it does not know** ("Pick a condition from the list.")
|
|
598
|
+
instead of silently rewriting it to "is" (`RuleFormInput` keeps the operator as posted); a saved rule's condition is
|
|
599
|
+
kept as stored on its edit page, which says the rule matches nothing. **No opt-out:** the form offers only the
|
|
600
|
+
conditions it knows.
|
|
601
|
+
<!-- pin: test/permission_forge/html/broken_rules_admin_test.rb#a posted condition that is not one is refused, never rewritten to "is" -->
|
|
602
|
+
<!-- pin: test/support/decision_parity/rules.rb#the operator is kept as posted, refused when it is none (no rewrite to equal) -->
|
|
603
|
+
- **A new record rule on a column that holds a secret is refused.** A rule whose column (after its association path,
|
|
604
|
+
qualified or not) matches `config.rule_secret_column_patterns` — digests, tokens, `encrypted_*`, secrets, passwords,
|
|
605
|
+
salts, API / private / access keys, OTP seeds — fails validation on create and when its column, path, join type or model changes
|
|
606
|
+
(`names a column that holds a secret …`), and the admin's rule builder no longer offers such columns (except the one
|
|
607
|
+
a stored rule already names). Rules stored before keep evaluating as they did and stay editable except their value
|
|
608
|
+
and operator; the audit (`secret_rule_columns`) and Home list them. **Opt out:** `config.rule_secret_column_patterns =
|
|
609
|
+
[]` (or a narrower list).
|
|
610
|
+
<!-- pin: test/permission_forge/models/permission_set_rule_secret_columns_test.rb#a new rule on a secret column is refused, with a message in every locale -->
|
|
611
|
+
- **`include_nulls` is off unless asked for — in code, through the JSON API and in the admin's rule builder alike.**
|
|
612
|
+
The column default (`true`) made a rule created in code or through the API let the NULL rows through
|
|
613
|
+
(`owner_id equal 5` also matched unassigned records) while the admin's builder started unchecked; the model now
|
|
614
|
+
declares the default (`attribute :include_nulls, :boolean, default: false`; the install migration writes `false`
|
|
615
|
+
too, and on an upgraded install the model's default wins over the column's). Rows already stored keep their value.
|
|
616
|
+
**Opt out:** pass `include_nulls: true` (code, API) or tick "Also show records where … is empty" in the builder — per
|
|
617
|
+
rule, as before.
|
|
618
|
+
<!-- pin: test/support/decision_parity/rules.rb#include_nulls has one default: off in code, through the API and in the builder -->
|
|
619
|
+
- **The rule builder offers every column of the table along the path, `id` included** (flagged "no field row
|
|
620
|
+
(ignored_fields)"), the same list the model's validation accepts (`RuleColumn.names_for`); a virtual field is never
|
|
621
|
+
offered. A rule on `id` — the docs' own example — can be built and repaired in the admin again. **No opt-out:** the
|
|
622
|
+
validation did not change; the admin stopped hiding columns it accepts.
|
|
623
|
+
<!-- pin: test/support/decision_parity/rules.rb#the builder offers exactly the columns the model accepts, ignored_fields included and flagged -->
|
|
624
|
+
- **The rule builder only offers associations a join can be built through.** `Admin::ModelCatalog#associations` keeps
|
|
625
|
+
the reflections `Operators::RulePath.unjoinable_cause` accepts: an association with a record-dependent scope, a
|
|
626
|
+
broken `:through` / `inverse_of`, a polymorphic one or one whose class cannot be loaded is no longer listed (a path
|
|
627
|
+
through it matched nothing and the model refused it on save). **No opt-out:** a rule through such an association
|
|
628
|
+
could never match a record.
|
|
629
|
+
<!-- pin: test/permission_forge/admin/services_test.rb#associations are the ones a join can be built through -->
|
|
630
|
+
- **RLS: `RlsPolicyGenerator::GENERATOR_VERSION` is 7** (5 in 0.14). A policy compiles every rule the application
|
|
631
|
+
layer matches nothing for — every `Operators::RuleProblem` kind — to `FALSE`, where such rules raised during the
|
|
632
|
+
sync or were skipped and left the policy wider than the application layer (a custom operator without SQL is still
|
|
633
|
+
skipped and reported). It types `uuid` / `inet` / `cidr` / native enum / `json` / array values like the app layer,
|
|
634
|
+
counts an assignment until its `expires_at` included (`>= now()`, as `PermissionSetUser.active_assignments`; it was
|
|
635
|
+
`>`), computes `today` / `now` / `tomorrow` / `yesterday` per statement (frozen at the sync before), grants the
|
|
636
|
+
bypass for View All alone (Modify All without it bypassed in the policy only), and only to a set without a condition as
|
|
637
|
+
`PermissionSet.unconditional` reads it (`conditions_present_sql`): a set with only an end hour no longer bypasses
|
|
638
|
+
the rules, and one whose condition texts are only blanks does. Existing installs see drift until they run `rake
|
|
639
|
+
permission_forge:rls:sync`. **No opt-out:** a policy is never wider than the application layer; the old policies
|
|
640
|
+
stay in force until the sync.
|
|
641
|
+
<!-- pin: test/permission_forge/operators/rule_problem_taxonomy_test.rb#unknown_operator: a row written past the model is named, denies, is listed, compiles to FALSE -->
|
|
642
|
+
<!-- pin: test/permission_forge/services/rls_policy_generator_test.rb#only unconditional sets grant the view_all bypass (consent guard) -->
|
|
643
|
+
<!-- pin: test/permission_forge/services/rls_policy_generator_test.rb#view_all bypass branch checks granting and muting sets -->
|
|
644
|
+
<!-- pin: test/rls/rls_rule_parity_test.rb#PostgreSQL's relative date words are read per statement by the app layer and by the policy -->
|
|
645
|
+
- **PostgreSQL: a `uuid`, `inet`, `cidr` or native enum value the column cannot hold matches nothing** where it raised
|
|
646
|
+
`ActiveRecord::StatementInvalid` for every holder (`find_by`, `permission_scoped`, every query under
|
|
647
|
+
`auto_query_scoping`) — `'banana'` against a `uuid` column, an address IPAddr cannot read against an `inet` column, an
|
|
648
|
+
address with host bits against a `cidr` column, a word that is not a label against a native enum column. Such a rule
|
|
649
|
+
is refused on save and listed by `rake permission_forge:audit` (`unusable_rule_values`, Medium while held) and on
|
|
650
|
+
Home. **No opt-out:** a value PostgreSQL cannot read never matched a row; a custom operator (an injected
|
|
651
|
+
`operator_registry:`) still receives the value as written.
|
|
652
|
+
<!-- pin: test/permission_forge/operators/postgres_column_types_test.rb#'banana' against a uuid column matches nothing, is refused on save and is listed by the audit -->
|
|
653
|
+
<!-- pin: test/permission_forge/operators/postgres_column_types_test.rb#each of these rules is named by RuleProblem and refused on save -->
|
|
654
|
+
- **PostgreSQL: a rule value the gem cannot type itself is asked of PostgreSQL, once per process** (a `CAST` in a
|
|
655
|
+
savepoint, `ValueCasters::PostgresqlTypes::ReadBack`): date and time text in another notation than ISO 8601
|
|
656
|
+
(`today`, `31 Jan 2026`) and every column type no caster knows that PostgreSQL can compare (`money`, `interval`,
|
|
657
|
+
`macaddr`, ...; `xml`, `point`, `hstore` cannot: no value, only the null checks). A timeout is not kept as unreadable.
|
|
658
|
+
What PostgreSQL reads is compared as written, as before; what it cannot read (`'banana'` for a date, `'noon'` for a
|
|
659
|
+
time) now matches nothing and is refused on save — it raised for every holder of the rule, on every query, and failed
|
|
660
|
+
`rake permission_forge:rls:sync`. `citext` is text. **No opt-out:** such a rule never matched anything; a custom
|
|
661
|
+
operator still gets the value as written.
|
|
662
|
+
<!-- pin: test/permission_forge/operators/postgres_column_types_test.rb#date text and types no caster knows are read back: what PostgreSQL cannot read matches nothing -->
|
|
663
|
+
<!-- pin: test/permission_forge/operators/postgres_column_types_test.rb#a type PostgreSQL cannot compare takes no value: xml and point match nothing, the null checks work -->
|
|
664
|
+
<!-- pin: test/permission_forge/operators/read_back_memo_test.rb#a timeout, a lock timeout or a lost connection denies one read and is asked again -->
|
|
665
|
+
- **PostgreSQL: a LIKE operator (`contains`, `starts_with`, ...) matches text columns only.** On an integer, decimal,
|
|
666
|
+
boolean, date, `uuid`, `inet`, `cidr`, native enum, `json` or other non-text column it matches nothing and is refused
|
|
667
|
+
on save — PostgreSQL has no LIKE for those, so the query raised for every holder (the RLS policy already compiled it
|
|
668
|
+
to `FALSE`). MySQL and SQLite, which compare such a column's text, are unchanged. **No opt-out:** such a rule never
|
|
669
|
+
matched anything.
|
|
670
|
+
<!-- pin: test/permission_forge/operators/postgres_column_types_test.rb#LIKE matches text columns only: an integer, a date or a boolean matches nothing instead of raising -->
|
|
671
|
+
- **A rule on a `json`, `jsonb` or array column matches nothing under every built-in operator but `is_null` /
|
|
672
|
+
`is_not_null`** — also an array or JSON literal that PostgreSQL used to compare as a whole document (`codes equal
|
|
673
|
+
'{1,2}'`), and on MySQL and SQLite, where a `json` column was compared as text. **Opt out:** register a custom
|
|
674
|
+
operator (`operator_registry:`), which receives the value as written.
|
|
675
|
+
<!-- pin: test/permission_forge/operators/postgres_column_types_test.rb#a json or array column matches nothing under every built-in operator but the null checks -->
|
|
676
|
+
- **An explicit field row for a variant name decides that name within its Resource.** An `owner_uuid` row next to an
|
|
677
|
+
`owner_id` row used to be OR-ed with the variant derived from `owner_id`, so an explicit Hidden `owner_uuid` read as
|
|
678
|
+
Editable in either row order. Now the row the admin wrote wins, whichever row is older; a derived variant only fills
|
|
679
|
+
a name no row of the resource has. Muting rows still mute their variants. The audit's `variant_rows_conflict` lists
|
|
680
|
+
the pairs before you upgrade. **No opt-out:** the explicit row is what the admin wrote.
|
|
681
|
+
<!-- pin: test/permission_forge/services/permission_hash_builder_test.rb#G5f: the owner_uuid entry is the explicit row when there is one, else the derived variant, in either order -->
|
|
682
|
+
- **A field variant gets a permission-hash entry only when the model has that name.** The default
|
|
683
|
+
`field_variant_mappings` (`_id` → `_uuid`, `_cents` → base name) added an entry for every mapped name, so a
|
|
684
|
+
`Customer` with an `owner_id` row and no `owner_uuid` column had an `owner_uuid` entry, which the admin's Effective
|
|
685
|
+
access listed and counted. A variant is now added (and muted) only when the model has it: a column or `attribute`,
|
|
686
|
+
an `alias_attribute`, a public reader or writer, or a declared virtual field; a model this app cannot load keeps
|
|
687
|
+
every variant. The hash format is unchanged, with fewer keys:
|
|
688
|
+
`permissions_hash_for(:Customer)[:fields][:owner_uuid]` is `nil` for a name the model lacks.
|
|
689
|
+
`FiltersResponseAttributes` still filters such a key of a serializer's hash by the entry of its base field.
|
|
690
|
+
**No opt-out:** none needed; if your code reads the entry of a computed variant, give the model the reader (or declare it
|
|
691
|
+
in `extra_fields_for_permissions`).
|
|
692
|
+
<!-- pin: test/permission_forge/field_variants_test.rb#the permission hash has entries for the real variants only -->
|
|
693
|
+
<!-- pin: test/permission_forge/field_variants_test.rb#a response key that is a variant the model lacks follows its base field -->
|
|
694
|
+
<!-- pin: test/permission_forge/admin/effective_access_test.rb#no variant field for a name the model does not have, in the engine or here -->
|
|
695
|
+
- **A grant counts only with the permission it depends on**, at read time as well as at save time: a field row with
|
|
696
|
+
`edit_access` but no `read_access`, a Resource row with `edit_access` / `delete_access` but no `read_access` or
|
|
697
|
+
`modify_all` without `view_all` (rows written past `ValidatesDependency`: SQL, an older version, another app) no
|
|
698
|
+
longer grant the dependent permission, and `PermitsAttributes` keeps a key only where the field is readable and
|
|
699
|
+
editable. The audit's `dependency_violations` lists such rows. **No opt-out:** the validation already forbade it.
|
|
700
|
+
<!-- pin: test/permission_forge/services/permission_hash_builder_test.rb#resource flags count edit and delete only with read, Modify All only with View All -->
|
|
701
|
+
- **A broken activation condition is unjudgeable in every mode.** An IP entry that is not an address, a list of
|
|
702
|
+
separators, one hour of the two, the same hour twice, or an hour outside 0–23 (stored past the validation) now makes
|
|
703
|
+
a **muting** set apply to every request (it never applied: a lockdown written with newlines through the API muted
|
|
704
|
+
nothing) and a **granting** set never pass, with or without a request context (a blank one let it grant). Called out: a granting set with **one hour of the two** used to pass (the
|
|
705
|
+
hours were ignored) and now never does; a granting set with `start == end` never passed before either. IP lists
|
|
706
|
+
written with newlines or spaces are now read entry by entry (they used to be one invalid entry). The audit's
|
|
707
|
+
`incomplete_conditions` lists every such set — run `rake permission_forge:audit` before upgrading; the admin's
|
|
708
|
+
Conditions tab and the API normalise the list on the next save. **No opt-out:** fix the condition on the set's
|
|
709
|
+
Conditions tab.
|
|
710
|
+
<!-- pin: test/permission_forge/conditional_sets_test.rb#a muting set with a broken condition applies in every mode; a granting one never passes -->
|
|
711
|
+
<!-- pin: test/permission_forge/concerns/conditional_sets_per_context_test.rb#a granting set with a broken condition grants nothing, not even without a context -->
|
|
712
|
+
- **A Permission Set's activation conditions are validated on a new row or a changed condition**: an `allowed_ips`
|
|
713
|
+
entry that is not an IP address or CIDR range, a window whose start equals its end, one hour of the two (en/sq/mk
|
|
714
|
+
messages); the stored list is normalised to commas on save, a text of whitespace alone (any Unicode space) to nil (a
|
|
715
|
+
row stored before keeps SQL's narrower reading); a copy keeps its original's. A row stored before the check stays
|
|
716
|
+
editable in its other attributes. **No opt-out:** such a condition cannot be judged (see above).
|
|
717
|
+
<!-- pin: test/permission_forge/models/permission_set_condition_rows_test.rb#a legacy row with a broken list stays editable until the list changes -->
|
|
718
|
+
<!-- pin: test/permission_forge/models/permission_set_condition_rows_test.rb#a new row with the same start and end hour is refused -->
|
|
719
|
+
<!-- pin: test/permission_forge/models/permission_set_condition_rows_test.rb#a condition text of whitespace alone is stored as nil, so Ruby and SQL read it the same -->
|
|
720
|
+
- **`PermissionSet#conditional?` and the `conditional` / `unconditional` scopes agree**: a text of blanks is no
|
|
721
|
+
condition (`TRIM`), one hour of the two is one. Counts on pages and in the audit may change for such rows.
|
|
722
|
+
**No opt-out:** two readings of one row.
|
|
723
|
+
<!-- pin: test/permission_forge/models/permission_set_condition_rows_test.rb#conditional? and the SQL scopes agree: whitespace is no condition, one hour of the two is -->
|
|
724
|
+
- **The admin's condition sentence names a broken hour window as one** ("between 09:00 and 09:00 (a window nobody can
|
|
725
|
+
judge — fix it on the Conditions tab)") instead of calling it overnight; the Conditions form refuses a start hour
|
|
726
|
+
equal to the end hour (error keyed `:hours`). **No opt-out:** wording.
|
|
727
|
+
<!-- pin: test/permission_forge/models/permission_set_condition_rows_test.rb#hours_window: none, a window, an overnight window, and the broken shapes -->
|
|
728
|
+
- **A mapped controller action no Permission Set manages is refused (403) unless `unconfigured_action_strategy = :allow`.** (issue #10)
|
|
729
|
+
`unconfigured_action_strategy` defaults to `:block` (it was `:allow`), in `Configuration.new` and so in the `:compat`
|
|
730
|
+
preset too. In a controller that includes `ControllerActionGuard`, an action that maps to a Resource no Permission Set
|
|
731
|
+
manages, or a custom action no Permission Set manages, raises `ActionNotAuthorizedError` (reason
|
|
732
|
+
`resource_not_managed_blocked` / `action_not_managed_blocked`, an `action.denied` event; 403 unless you rescue it)
|
|
733
|
+
instead of running for every signed-in user — a new action nobody configured is refused until someone decides who
|
|
734
|
+
may run it, as an unconfigured field already was. Unchanged: actions that map to no Resource are never checked, a
|
|
735
|
+
request with nobody signed in follows `action_guard_nil_user_strategy`, `action_guard_bypass` and grants come
|
|
736
|
+
first. Every refused request stores an `action.denied` audit event (the denied group is stored by default), so a
|
|
737
|
+
page left unmanaged fills the audit log. `rake permission_forge:audit` lists the guarded actions this refuses
|
|
738
|
+
(`action/unmanaged_actions_refused`, Low: per Resource not managed, then per custom action; with tenancy, each with
|
|
739
|
+
the tenants that do not manage it), read from the routes whether or not `controller_action_discovery` is on — run it
|
|
740
|
+
before the deploy; a CI step running it with `FAIL_ON=low` (or `info`) now fails on that finding. It cannot see a
|
|
741
|
+
per-action `skip_before_action :pf_authorize_action!, only: …` (those actions are listed although they run), and
|
|
742
|
+
`action/actions_not_checked` (Low) names the controller actions it could not judge (a controller that fails to load,
|
|
743
|
+
a raising resolver or mapping). The admin's Actions tab says "Not managed — blocked until a set permits it", and its
|
|
744
|
+
note and review say the first row opens an action to the people permitted. If you copied
|
|
745
|
+
`config.unconfigured_action_strategy = :allow` from the 0.14 reference block of docs/configuration.md, remove it to
|
|
746
|
+
get the new default. **Opt out:** `config.unconfigured_action_strategy = :allow` restores the 0.14 behaviour;
|
|
747
|
+
it fails open, so the audit reports it (`config/unconfigured_actions_allowed`, Medium, and
|
|
748
|
+
`action/unmanaged_actions_allowed`, Info, listing the actions it opens), the Settings page flags it and `:strict`
|
|
749
|
+
counts it as an override.
|
|
750
|
+
<!-- pin: test/permission_forge/controller_actions/guard_integration_test.rb#an action nobody lists is refused by default and runs under :allow -->
|
|
751
|
+
<!-- pin: test/permission_forge/controller_actions/guard_integration_test.rb#a resource no Permission Set manages is refused by default and runs under :allow -->
|
|
752
|
+
<!-- pin: test/permission_forge/configuration_test.rb#controller action options default to discovery off and block -->
|
|
753
|
+
<!-- pin: test/permission_forge/services/security_audit_action_check_test.rb#guarded actions no Permission Set manages are listed as refused, per resource and per action -->
|
|
754
|
+
<!-- pin: test/permission_forge/services/security_audit_test.rb#an explicit :allow for unconfigured actions is a medium finding -->
|
|
755
|
+
<!-- pin: test/permission_forge/services/security_audit_action_check_test.rb#with tenancy, an action is listed with the tenants that do not manage it -->
|
|
756
|
+
<!-- pin: test/permission_forge/services/security_audit_action_check_test.rb#controller actions the catalog could not judge are reported, and the rest still checked -->
|
|
757
|
+
<!-- pin: test/permission_forge/html/action_discovery_test.rb#under :allow, review warns that the first row restricts the action -->
|
|
758
|
+
- **An unrescued denial answers 403 Forbidden instead of 500.** The engine registers
|
|
759
|
+
`PermissionForge::Concerns::PermissionScopedFindable::NotAuthorizedError` and
|
|
760
|
+
`PermissionForge::ActionNotAuthorizedError` in `config.action_dispatch.rescue_responses` as `:forbidden`, as
|
|
761
|
+
ActiveRecord registers `RecordNotFound` as 404. In production Rails renders `public/403.html`, which Rails apps do
|
|
762
|
+
not ship (an HTML request gets an empty 403 page until you add one; JSON gets `{"status":403,"error":"Forbidden"}`).
|
|
763
|
+
Like every mapped exception, an unrescued denial is no longer reported to `Rails.error` as unhandled; it stays in
|
|
764
|
+
the audit log (`access.denied`, `action.denied`). Request tests under `show_exceptions = :rescuable` get the 403
|
|
765
|
+
instead of the raised error. A `rescue_from` in your controllers still decides first. **Opt out:** rescue the errors
|
|
766
|
+
yourself, or set another status, e.g.
|
|
767
|
+
`config.action_dispatch.rescue_responses['PermissionForge::ActionNotAuthorizedError'] = :internal_server_error`.
|
|
768
|
+
<!-- pin: test/permission_forge/controllers/denial_status_test.rb#both denials are registered as 403 where ActionDispatch reads them -->
|
|
769
|
+
<!-- pin: test/permission_forge/controllers/denial_status_test.rb#an unrescued row-rule denial answers 403 with an empty page, and JSON for a JSON request -->
|
|
770
|
+
<!-- pin: test/permission_forge/controllers/denial_status_test.rb#a host rescue_from still decides, and reads the rule messages without parsing -->
|
|
771
|
+
- **Permission Set names are unique within a tenant, ignoring case and surrounding spaces.** The admin form already
|
|
772
|
+
refused a second "Sales Team"; the console, seeds, services and the API created one silently, and both then showed
|
|
773
|
+
as "Sales Team" in every picker. A new set or a rename to a name another set of the same tenant uses is now a
|
|
774
|
+
validation error (`name_taken`). Two sets that already share a name stay valid until one of them is renamed. A
|
|
775
|
+
clone without a name (`PermissionSetCloner`, the group clone, `POST …/clone`) gets a numbered copy name ("Sales
|
|
776
|
+
Team (copy)", "Sales Team (copy) 2"). No migration: the check is a validation. **No opt-out:** give duplicates
|
|
777
|
+
distinct names.
|
|
778
|
+
<!-- pin: test/permission_forge/models/permission_set_name_uniqueness_test.rb#a second set with the same name is refused, whatever its case -->
|
|
779
|
+
<!-- pin: test/permission_forge/models/permission_set_name_uniqueness_test.rb#two sets that already share a name stay valid until one is renamed -->
|
|
780
|
+
<!-- pin: test/permission_forge/models/permission_set_name_uniqueness_test.rb#a clone without a name gets a numbered copy name instead of failing validation -->
|
|
781
|
+
- **Full access applies no personal-data masks.** `ResourceFieldGenerator.call(grant_all: true)`, and through it
|
|
782
|
+
`Resource.grant_full_access!`, `Services::ResourceWithFieldsBuilder` and the admin API's `generate_fields` with
|
|
783
|
+
`grant_all`, now create every field readable and editable in the clear, also the columns that match
|
|
784
|
+
`gdpr_field_patterns` under `gdpr_defaults`. Before, those were created masked (a field test's Admin set read
|
|
785
|
+
customer names as `G*************tion`). The recommended strategy is still stored in `mask_strategy` (with
|
|
786
|
+
`mask_access: false`), so masking the field later uses it, and the fields are still flagged: `GdprFieldAuditor` and
|
|
787
|
+
`rake permission_forge:audit_masks` report them as `unmasked_field`, the admin's Home lists them under "Privacy
|
|
788
|
+
audit". (Without `grant_all`, see the next entry.) **Opt out:** generate without `grant_all` and open the other
|
|
789
|
+
fields, or mask the reported fields afterwards (`resource.resource_fields.where(name: %w[name email]).find_each { |f|
|
|
790
|
+
f.update!(mask_access: true) }`; the stored strategy applies).
|
|
791
|
+
<!-- pin: test/permission_forge/services/resource_field_generator_gdpr_test.rb#grant_all true with gdpr_defaults true leaves the email field unmasked -->
|
|
792
|
+
<!-- pin: test/permission_forge/models/resource_test.rb#.grant_full_access! applies no personal-data masks under gdpr_defaults -->
|
|
793
|
+
<!-- pin: test/permission_forge/api/resources_controller_test.rb#POST generate_fields with grant_all applies no masks under gdpr_defaults -->
|
|
794
|
+
- **Generating fields without `grant_all` grants nothing — the personal-data columns included.**
|
|
795
|
+
`ResourceFieldGenerator.call(resource:)`, the API's `POST …/generate_fields` without `grant_all` and the console
|
|
796
|
+
used to create, under `gdpr_defaults`, the columns that look like personal data *readable and masked* while every
|
|
797
|
+
other column was Hidden. They are now Hidden like the rest, with the recommended mask strategy on the row — the same
|
|
798
|
+
row the admin's "Nothing" preset, "Add all new as Hidden" and bulk Hidden write. Rows already written are not
|
|
799
|
+
changed. **Opt out:** to have them readable and masked, add them Read-only (the admin's Sync "Add all new as
|
|
800
|
+
Read-only", bulk Read, or the field drawer: under `gdpr_defaults` a sensitive column granted Read-only starts
|
|
801
|
+
Masked), or generate with `grant_all: true` and mask what the audit lists.
|
|
802
|
+
<!-- pin: test/permission_forge/services/resource_field_generator_gdpr_test.rb#grant_all false with gdpr_defaults true grants nothing: the email field is Hidden, its strategy recorded -->
|
|
803
|
+
- **Generating fields on a Muting Permission Set writes the level chosen, and no row without `grant_all`.** Without
|
|
804
|
+
`grant_all` the generator wrote, under `gdpr_defaults`, `read_access: true, mask_access: true` rows for the
|
|
805
|
+
personal-data columns of a muting set — rows that *hide* those columns for the set's holders and force a mask — and
|
|
806
|
+
an Unchanged row (every flag off) for every other column, where the admin's presets and bulk add write none.
|
|
807
|
+
`ResourceFieldGenerator.call(resource:)` (the API's `generate_fields`, the console) now writes no row on a muting set
|
|
808
|
+
(Unchanged mutes nothing, which is what no row means: `FieldLevel.row?`), and `grant_all: true` still writes the
|
|
809
|
+
Hide rows. Rows already written are not changed (the audit's `dependency_violations` and Home list the privacy
|
|
810
|
+
ones). **No opt-out:** a muting row written by a privacy default was a silent removal nobody chose, and the rows it
|
|
811
|
+
no longer writes took nothing away.
|
|
812
|
+
<!-- pin: test/support/decision_parity/fields.rb#(a) Customer, muting=true, gdpr_defaults=true, leave: presets, bulk and the generator write the same rows -->
|
|
813
|
+
<!-- pin: test/permission_forge/services/resource_field_generator_test.rb#without grant_all a Muting Permission Set gets no rows at all, the timestamps included -->
|
|
814
|
+
- **Every field-creation path writes the same names and caps Editable alike.** The generator also writes rows for
|
|
815
|
+
virtual fields under `extra_fields_default_access = :grant` (Hidden without `grant_all`, replacing the implicit
|
|
816
|
+
grant), and Editable on a Resource without Edit is Read-only on every path, after the privacy default
|
|
817
|
+
(`ManagedFields`, `FieldLevel.capped`). **Opt out:** give the Resource Edit first; `grant_all` keeps virtual fields readable.
|
|
818
|
+
<!-- pin: test/support/decision_parity/fields.rb#(a) Editable on a Resource without Edit is Read-only on every path, and the privacy default does not mask it -->
|
|
819
|
+
<!-- pin: test/support/decision_parity/fields.rb#(a) a declared virtual field gets the level chosen on every path under extra_fields_default_access = :grant -->
|
|
820
|
+
- **The admin's "Full access" masks nothing.** Adding a resource with the **Full access** preset, or Sync's "Add all
|
|
821
|
+
new with full access", now gives every field Editable, also the columns that look like personal data under
|
|
822
|
+
`gdpr_defaults` (the field test's `Customer.name` started Masked). Those columns keep the **Sensitive** badge and
|
|
823
|
+
their recommended mask strategy, and Home's Privacy audit lists them as shown unmasked, with **Fix** and **This is
|
|
824
|
+
intended**. **Read-only** (preset and Sync) still starts them Masked; the muting presets are unchanged. **Opt out:**
|
|
825
|
+
add the resource with **Read-only** and raise the other fields to Editable, or set the reported fields to Masked
|
|
826
|
+
from the Fields tab (bulk: filter **Sensitive**, select all, Set to Masked).
|
|
827
|
+
<!-- pin: test/permission_forge/html/full_access_privacy_test.rb#the Full access preset masks nothing and keeps the recommended strategy -->
|
|
828
|
+
<!-- pin: test/permission_forge/html/full_access_privacy_test.rb#sync: "with full access" masks nothing, "Read-only" masks the sensitive columns -->
|
|
829
|
+
- **Generated rows always carry the recommended mask strategy** of a sensitive column (`email` for `email`,
|
|
830
|
+
`first1_last4` for `name`), also with `gdpr_defaults` off and also on a muting set, as the admin's rows already did;
|
|
831
|
+
it is used only when someone masks the field. **No opt-out:** the flags are unchanged.
|
|
832
|
+
<!-- pin: test/permission_forge/services/resource_field_generator_test.rb#without grant_all gdpr_defaults records the recommended strategy and grants nothing: every row Hidden -->
|
|
833
|
+
- **Generated fields give `created_at` / `updated_at` a row, at most read-only.** They were in the default
|
|
834
|
+
`ignored_fields`, so they got no row and, under the default `unconfigured_field_strategy = :block`, every reader saw
|
|
835
|
+
`null` timestamps. `ignored_fields` no longer lists them, and every path that generates rows (the generator,
|
|
836
|
+
`Resource.grant_full_access!`, the API's `generate_fields`, the admin's presets, Sync and bulk add) gives them one
|
|
837
|
+
through `TimestampFields`: on a granting set, read-only unmasked for a level that grants read, hidden otherwise; on
|
|
838
|
+
a muting set, the level chosen. **What an existing install sees:** right after the upgrade the admin lists
|
|
839
|
+
`created_at` / `updated_at` as not configured on every resource until its fields are synced. **Opt out:**
|
|
840
|
+
`config.ignored_fields += %w[created_at updated_at]`.
|
|
841
|
+
<!-- pin: test/permission_forge/services/resource_field_generator_test.rb#a user granted the generated fields reads the timestamps under :block, and cannot edit them -->
|
|
842
|
+
<!-- pin: test/permission_forge/admin/column_fields_test.rb#a level that grants read gives the timestamps Read-only, never Editable or Masked -->
|
|
843
|
+
<!-- pin: test/permission_forge/configuration_test.rb#has default ignored_fields -->
|
|
844
|
+
- **The GDPR audit, the security audit and the boot warning also count the columns your `filter_parameters` name.**
|
|
845
|
+
While `filter_parameters_integration` is on (the default), a column the host's `filter_parameters` name
|
|
846
|
+
(`api_token`, `license_key`, …) readable in the clear is reported as `unmasked_field` / `unmasked_personal_data`
|
|
847
|
+
with the suggested strategy `full`, and named by the boot warning — as the admin's presets and Sync already masked
|
|
848
|
+
it on Read-only. A String / Symbol entry names a column when a `_`-segment of it starts with the entry (`:otp`:
|
|
849
|
+
`otp_secret`, not `footprint`; `:_key`: `api_key`, also `sort_key`); a Regexp matches as written. **Opt out:**
|
|
850
|
+
`config.filter_parameters_integration = false`, or mark the field "This is intended" in the admin's privacy audit.
|
|
851
|
+
<!-- pin: test/support/decision_parity/fields.rb#(b) the GDPR audit and the boot warning name exactly the columns SensitiveColumns matches, with its strategy -->
|
|
852
|
+
<!-- pin: test/support/decision_parity/fields.rb#(a) a column named only by the host filter_parameters starts Masked on every path (Read-only, gdpr_defaults) -->
|
|
853
|
+
- **The boot warning about unmasked personal data counts the rows that reach someone.** `unmasked_gdpr_fields` used
|
|
854
|
+
to name any row with `mask_access: false` — Hidden rows, switched-off rows, rows of a muting set. It now names a
|
|
855
|
+
row that is readable in the clear, in effect, in a granting set (`ResourceField.exposing_in_clear`). **No opt-out:**
|
|
856
|
+
a warning about a row that shows nothing was noise.
|
|
857
|
+
<!-- pin: test/support/decision_parity/fields.rb#(b) the GDPR audit and the boot warning name exactly the columns SensitiveColumns matches, with its strategy -->
|
|
858
|
+
- **`email_address` is detected as an email.** `DEFAULT_GDPR_FIELD_PATTERNS` adds `email_address` and
|
|
859
|
+
`*_email_address` (the column Rails 8's authentication generator creates) to the email pattern, so it is recommended
|
|
860
|
+
and generated with the `email` strategy (`j***@example.com`) instead of `full` (it matched `address`). Log filtering
|
|
861
|
+
is unchanged. Existing field rows are never rewritten (a row generated with `full` keeps the stricter mask); `rake
|
|
862
|
+
permission_forge:audit_masks` suggests `email` for an unmasked one. Apps that set `gdpr_field_patterns` to their own
|
|
863
|
+
hash keep their patterns; apps that merged into `DEFAULT_GDPR_FIELD_PATTERNS` get the new alternatives. **Opt out:**
|
|
864
|
+
put a pattern ahead of the defaults: `config.gdpr_field_patterns = { /\Aemail_address\z/ => 'full'
|
|
865
|
+
}.merge(PermissionForge::Configuration::DEFAULT_GDPR_FIELD_PATTERNS)`.
|
|
866
|
+
<!-- pin: test/permission_forge/gdpr_default_patterns_test.rb#email_address is detected as an email, everywhere the patterns are read -->
|
|
867
|
+
- **The admin says a privacy flag may not be personal data.** Detection is unchanged (by column name, for every
|
|
868
|
+
model: `name` on a company model is still flagged). The field drawer's recommendation now reads "Matches privacy
|
|
869
|
+
pattern “name”, so it may be personal data", and Home's Privacy audit says that fields are flagged by column name
|
|
870
|
+
and that **This is intended** takes a field that is fine unmasked off the list. **No opt-out:** wording only.
|
|
871
|
+
<!-- pin: test/permission_forge/html/full_access_privacy_test.rb#a flag may not be personal data: the card says so, and This is intended clears it -->
|
|
872
|
+
- **A Muting Permission Set's column with no setting is "Unchanged" in the admin.** Its Fields tab counted such
|
|
873
|
+
columns under "Not configured" next to an "Unchanged 0" chip, and their level control had no segment selected,
|
|
874
|
+
although "Not configured · Mutes nothing" and Unchanged are one state. The row now starts on Unchanged (saving it
|
|
875
|
+
still creates no field row), the muting Fields filters drop "Not configured", and the field drawer and "Across all
|
|
876
|
+
Permission Sets" say Unchanged too (`FieldLevel.unconfigured_level`). A turned-off row keeps its saved choice and
|
|
877
|
+
its "Turned off · Mutes nothing" badge. Granting sets are unchanged. **No opt-out:** wording and counts only.
|
|
878
|
+
<!-- pin: test/permission_forge/html/field_test_round_three_admin_test.rb#in a Muting Permission Set a column with no setting is on Unchanged, counted under Unchanged -->
|
|
879
|
+
<!-- pin: test/permission_forge/html/field_test_round_three_admin_test.rb#saving a muting column left on Unchanged creates no field row -->
|
|
880
|
+
<!-- pin: test/permission_forge/html/state_rendering_test.rb#a column with no setting: Not configured on a granting set, Unchanged on a muting one, alike everywhere -->
|
|
881
|
+
- **The admin words each state one way.** Every field level, effective outcome, CRUD cell, action state, lock and
|
|
882
|
+
actor kind renders from `Admin::StateLabel` (glyph and word together). Visible differences: "Across all
|
|
883
|
+
Permission Sets" shows a muting set's Hide as "⊘ Hide" (was "Muted: hidden"), its Force mask with the strategy;
|
|
884
|
+
the Effective access legend reads "⊘ Blocked" and "– No", as its cells do (was "Muted" / "Not given"); the Fields
|
|
885
|
+
tab's filter for Hide reads "Hide" (was "Hidden"). Locale keys moved: `ui.levels.*` → `ui.states.field_level.*`,
|
|
886
|
+
`ui.states.{granted,blocked,none}` → `ui.states.effective.*`, `people.effective_tab.hidden_by_muting` →
|
|
887
|
+
`ui.states.effective.hidden_by_muting`, `audit_log.actor_types.*` → `ui.states.actor.*`, `ui.badges.locked` →
|
|
888
|
+
`ui.states.lock.locked`, `shared.crud_cell.needs_read` → `ui.needs_read_first`; removed `ui.levels.muted`,
|
|
889
|
+
`ui.badges.not_configured`, `ui.audit.system`, `people.effective_tab.legend_muted` / `legend_none`,
|
|
890
|
+
`shared.across_sets.muted_hidden` and the level keys under `resources.fields_tab.filters`. **Opt out:** a host that
|
|
891
|
+
overrides one of these keys in its own locale files renames it to the new key; the gem's own locales need nothing.
|
|
892
|
+
<!-- pin: test/permission_forge/i18n_test.rb#one state, one label: no English label repeats within the label namespaces -->
|
|
893
|
+
<!-- pin: test/permission_forge/html/state_rendering_test.rb#a column with no setting: Not configured on a granting set, Unchanged on a muting one, alike everywhere -->
|
|
894
|
+
- **Settings' health summary reads Home.** While Home lists items that need attention, the summary says how many
|
|
895
|
+
("Home lists 3 items that need attention.", with a link to Home) and no longer says the settings "look good"
|
|
896
|
+
("N settings are set as recommended" instead); "look good" is said only with no risky setting and nothing on
|
|
897
|
+
Home — it said "7 settings look good" while Home warned that automatic query scoping leaves record rules to
|
|
898
|
+
`find_by` / `permission_scoped`, which Settings now flags too. `unconfigured_field_strategy = :warn` is now a
|
|
899
|
+
flagged row, as `:pass_through` was (it lets an unconfigured field through too). **No opt-out:** wording and a flag;
|
|
900
|
+
nothing is enforced differently.
|
|
901
|
+
<!-- pin: test/permission_forge/html/labels_and_attention_admin_test.rb#Settings never says the settings look good while Home lists items that need attention -->
|
|
902
|
+
<!-- pin: test/permission_forge/html/admin_behaviour_test.rb#Settings warns about automatic query scoping while Home lists rules it leaves out -->
|
|
903
|
+
- **A host model, column, association or action without a translation is never inflected into English on a page in
|
|
904
|
+
another language.** "Full address", "Created at", "Customers", "Archive" and "Supplier" on a Macedonian or Albanian
|
|
905
|
+
page read `full_address`, `created_at`, "Customer records" (`permission_forge.ui.resource_plural`), `archive`,
|
|
906
|
+
`supplier`, in every admin screen and in `Admin::ModelCatalog`'s labels (the rule builder, Sync, "Add resource"); an
|
|
907
|
+
English page keeps Rails' own English for a column or action name ("Created at", `human_attribute_name`), and names
|
|
908
|
+
a model by its constant. `ResourceAction#display_name` without a label is the key (Rails' English on an English
|
|
909
|
+
page). **Opt out:** translate your models and columns (`activerecord.models.<model>` with `one`/`other`,
|
|
910
|
+
`activerecord.attributes.<model>.<name>`, [docs/i18n.md](docs/i18n.md#your-models-and-columns-in-the-admin)) or give
|
|
911
|
+
the row a label; there is no switch back to `humanize` on a page that is not English.
|
|
912
|
+
<!-- pin: test/permission_forge/admin/labels_test.rb#a model is labelled by the Resource row, else the host translation, else the constant name; the plural likewise -->
|
|
913
|
+
<!-- pin: test/permission_forge/admin/services_test.rb#labels are the host translation, else the stored name verbatim (Rails' English on an English page) -->
|
|
914
|
+
- **Every validation message of the gem's models is the gem's own, in the request's locale.** Rails' default messages
|
|
915
|
+
(`uniqueness`, `length`, `numericality`, `presence`, `ValidatesDependency`, `HasUuid`) and the messages that were
|
|
916
|
+
translated once at class load read from `permission_forge.validations.*` at validation time: `blank`, `taken`,
|
|
917
|
+
`too_long`, `requires`, `permission_set.hours_range` ("must be between 0 and 23"), `resource.name_taken`,
|
|
918
|
+
`permission_set_user.taken` ("already holds this Permission Set"), `permission_set_group_user.taken` ("is already a
|
|
919
|
+
member of this group"). The English wording changes for the two assignment uniqueness messages (was "has already
|
|
920
|
+
been taken") and the hours range (was "must be in 0..23"). The audit log's refused filters (`Audit::EventQuery`) say
|
|
921
|
+
`validations.blank` / `validations.invalid`. **No opt-out:** these replace Rails-core keys that are English, or
|
|
922
|
+
"Translation missing", in every host without rails-i18n.
|
|
923
|
+
<!-- pin: test/permission_forge/html/locale_leak_test.rb#every API error class answers in the request language -->
|
|
924
|
+
- **A validation sentence in the admin names the attribute with the gem's label.** The uniqueness race flash and
|
|
925
|
+
`Admin::ChangeSet::Invalid` say "Person already holds this Permission Set" (`Admin::ErrorSentence`), not Rails'
|
|
926
|
+
`full_messages` ("User has already been taken"). **No opt-out:** `full_messages` is English without rails-i18n.
|
|
927
|
+
<!-- pin: test/permission_forge/unique_conflict_test.rb#the real error of each indexed table is answered with the message its validation gives -->
|
|
928
|
+
- **The rule builder starts a new rule's name and blocked message blank** instead of the install migration's English
|
|
929
|
+
column defaults, which admins of an Albanian or Macedonian admin saved for their end users; a rule left without a
|
|
930
|
+
blocked message denies with `permission_forge.denials.rule_default` in the current locale (see
|
|
931
|
+
`NotAuthorizedError#failure_messages` under Added). **Opt out:** write a `failure_message` on the rule; the stored
|
|
932
|
+
English default of rules created before 0.15 stays until edited (docs/i18n.md
|
|
933
|
+
[Column Defaults Are English](docs/i18n.md#column-defaults-are-english) has the one-liner that blanks them).
|
|
934
|
+
<!-- pin: test/permission_forge/release_readiness/column_default_prefill_test.rb#no new-record screen or re-rendered form pre-fills a text default -->
|
|
935
|
+
<!-- pin: test/permission_forge/concerns/permission_scoped_findable_test.rb#NotAuthorizedError#failure_messages are the denying rules' own messages, without parsing -->
|
|
936
|
+
- **`pf_user_option_label`** (the group "Add member" select) is `Admin::PersonLabel`'s answer: a formatter that
|
|
937
|
+
returns blank falls through to the name as everywhere else in the admin (was "User #id"); the default
|
|
938
|
+
`profile_builder` reads `name` raw-column-first (`PersonLabel.raw_value`) past a masked User model. **No opt-out:**
|
|
939
|
+
one definition of a person's label.
|
|
940
|
+
<!-- pin: test/permission_forge/helpers/application_helper_test.rb#pf_user_option_label falls through to the name when the formatter returns blank (Admin::PersonLabel) -->
|
|
941
|
+
- **Settings draws configuration values as `<code>`** (the enum pills, the key → value map, the default and the docs
|
|
942
|
+
link's key); the locale keys `settings.row.default` and `settings.row.docs` became `default_html` and `docs_html`.
|
|
943
|
+
**Opt out:** a host that overrode the two keys renames them.
|
|
944
|
+
<!-- pin: test/permission_forge/html/locale_leak_test.rb#every screen shows only translated words and host data -->
|
|
945
|
+
- **Three placeholders are short hints**: `permission_forge.ui.search_people` and `home.index.search_placeholder`
|
|
946
|
+
read "Name or email" (sq "Emër ose email", mk "Име или е-пошта"), the Home explanation moved to the new
|
|
947
|
+
`home.index.search_help` under the search box, and `permission_set_rules.builder.message_placeholder` became
|
|
948
|
+
`permission_set_rules.builder.message_help`, a hint under the blocked-message textarea. **Opt out:** a host that
|
|
949
|
+
wants the old sentences overrides these keys in its own locale files (a placeholder longer than 40 Latin / 34
|
|
950
|
+
Cyrillic characters clips on a phone).
|
|
951
|
+
<!-- pin: test/permission_forge/html/layout_lint_test.rb#every placeholder fits a phone-width box in every locale -->
|
|
952
|
+
- **"Refresh cached permissions" says when it is needed.** The person page's menu read "Rarely needed: changes
|
|
953
|
+
already apply immediately.", which is wrong with a per-process cache (a memory store) after a change from a
|
|
954
|
+
console, runner or job: the process that made the change cleared only its own entries, and the server keeps the old
|
|
955
|
+
permissions until they expire. It now says that (en, sq, mk), in the words `Cache::Outcome` reports.
|
|
956
|
+
**No opt-out:** text only.
|
|
957
|
+
<!-- pin: test/permission_forge/html/field_test_round_three_admin_test.rb#Refresh cached permissions says when it is needed, in every locale -->
|
|
958
|
+
- **"What people lose" (Remove, Remove all, a set's impact line) counts the resources in effect**
|
|
959
|
+
(`Resource.in_effect`: active rows of an active set), not every active row. **No opt-out:** a label.
|
|
960
|
+
<!-- pin: test/permission_forge/release_readiness/one_definition_guard_test.rb#one definition: the resources that count right now (Resource.in_effect), also for "what people lose" -->
|
|
961
|
+
- **Boot and configuration warnings reach the terminal where the log is not read.** Every boot/configuration warning
|
|
962
|
+
of the gem (the per-process cache, conditional sets with an empty context, the API without authorization lambdas,
|
|
963
|
+
unmasked GDPR fields, missing or stale RLS policies, `audit_events = false`, ...) goes through one emitter,
|
|
964
|
+
`PermissionForge::Warnings.emit`: the log always; standard error as well in development and, outside test, in any
|
|
965
|
+
process that is not a web server (a console, a runner, a rake task, a job worker) — once per warning per process,
|
|
966
|
+
and not when the logger already writes to standard output or error. Before, only the development cache warnings
|
|
967
|
+
reached standard error, and every SECURITY warning (a fail-open condition) was in the log alone. The warnings about
|
|
968
|
+
the admin and the API stay in the log outside a server process (next entry). **No opt-out:** one line per warning per
|
|
969
|
+
process, only where the log is not on the terminal; each warning names the setting that removes its condition.
|
|
970
|
+
<!-- pin: test/permission_forge/boot_warnings_channel_test.rb#the emitter writes where the policy table says -->
|
|
971
|
+
<!-- pin: test/permission_forge/cache_warnings_test.rb#development writes both warnings to standard error as well, once each -->
|
|
972
|
+
- **The warnings about the admin and the API are printed by the server, not by every runner.** HTTP Basic auth
|
|
973
|
+
without credentials ("The admin UI will return 401 for all requests"), a shared Basic-auth login without
|
|
974
|
+
`audit_actor_resolver`, an API-only host missing the admin's middlewares, tenantless admin access and the API without
|
|
975
|
+
its authorization lambdas went to standard error in every `bin/rails runner`, rake task, console and `db:seed`, which
|
|
976
|
+
serve no admin — and with the README's credentials set for the server only, the 401 sentence was untrue there.
|
|
977
|
+
`Warnings.emit` takes `audience: :server` (beside `severity:`): such a warning goes to the log only outside a server
|
|
978
|
+
process; the server prints it as before. Every other warning keeps reaching every process. A server process is now
|
|
979
|
+
also one started by `unicorn` / `unicorn_rails` (`Unicorn::Launcher`), Passenger (`PhusionPassenger::App`), `falcon`
|
|
980
|
+
(`Falcon::Command`), `pitchfork` or `rackup` (by program name: the name the process was started with,
|
|
981
|
+
`Process.argv0`, which pitchfork's process titles do not change, or `$0`, which `bundle exec rackup` sets), as by
|
|
982
|
+
`bin/rails server` and `puma`; a server started some other way is not recognised (docs/configuration.md "Where
|
|
983
|
+
warnings appear"). In production these write
|
|
984
|
+
every warning to the log only, as Puma's processes do (they wrote them to standard error), and a change their host
|
|
985
|
+
controllers make reports `in_request: true` in its cache outcome (`Cache::Report.in_request?`). **No opt-out:** the
|
|
986
|
+
log keeps every warning; a runner that must check the admin's setup reads it there.
|
|
987
|
+
<!-- pin: test/permission_forge/install/runner_warnings_test.rb#bin/rails runner keeps the admin warnings off standard error; the log has them, the other warnings print -->
|
|
988
|
+
<!-- pin: test/permission_forge/install/runner_warnings_test.rb#bin/rails server prints the admin warnings -->
|
|
989
|
+
<!-- pin: test/permission_forge/boot_warnings_channel_test.rb#every warning has its audience in AUDIENCE, the one its site passes, and a reason when it is server -->
|
|
990
|
+
<!-- pin: test/permission_forge/boot_warnings_channel_test.rb#a server warning is written where the server policy table says -->
|
|
991
|
+
<!-- pin: test/permission_forge/boot_warnings_channel_test.rb#a server process is one bin/rails server or a web server command started -->
|
|
992
|
+
<!-- pin: test/permission_forge/boot_warnings_channel_test.rb#a process the pitchfork or rackup command started is a server, by its original program name -->
|
|
993
|
+
- **Nothing is checked or logged while `bin/rails generate` or `destroy` boots the app.** The install generator boots
|
|
994
|
+
the app before it writes the initializer and the migration that creates the audit table, and that boot logged six
|
|
995
|
+
`[PermissionForge]` lines describing the defaults (the in-memory cache, HTTP Basic Auth without credentials, masking
|
|
996
|
+
not complete) and told a fresh install to run `permission_forge:upgrade`. While a generator runs
|
|
997
|
+
(`Warnings.generator_running?`: the `generate` / `destroy` command is loaded) the boot checks, their warnings, the
|
|
998
|
+
missing-table check (the first audit event checks again), the "Added N GDPR patterns" line, `PermissionForge.configure`'s
|
|
999
|
+
model-binding warning and the masking machinery keep quiet; `validate_gdpr_explicit_choice!` still raises.
|
|
1000
|
+
**No opt-out:** the next server, console or task boot checks and warns about the configuration the app has.
|
|
1001
|
+
<!-- pin: test/permission_forge/boot_warnings_channel_test.rb#nothing is written while a generator boots the app -->
|
|
1002
|
+
<!-- pin: test/permission_forge/engine_boot_paths_test.rb#BootChecks.run checks and logs nothing while a generator boots the app -->
|
|
1003
|
+
<!-- pin: test/permission_forge/audit/boot_warnings_test.rb#no missing-table warning while a generator runs; the first event checks again -->
|
|
1004
|
+
- **The development masking note is written to standard error too** (with `auto_apply_masked_attributes = :auto` and a
|
|
1005
|
+
per-process or null cache store): it says the server re-reads the `Resource` names from the database in development
|
|
1006
|
+
and that production would mask a model first named in another process only after a restart. It was in the log only.
|
|
1007
|
+
**Opt out:** share the store (`config.cache_store = :file_store, "tmp/cache/store"` in development), list the models
|
|
1008
|
+
in `auto_apply_masked_attributes`, or include `MaskedAttributes` in the models with `auto_apply_masked_attributes =
|
|
1009
|
+
false` — each removes the condition, and the warning with it.
|
|
1010
|
+
<!-- pin: test/permission_forge/cache_warnings_test.rb#development writes both warnings to standard error as well, once each -->
|
|
1011
|
+
- **The production boot warning about an in-process cache also covers a `:memory_store`.** It fired only for
|
|
1012
|
+
`cache_backend = :memory`; `cache_backend = :rails_cache` over a `Rails.cache` that is a `:memory_store` (or a
|
|
1013
|
+
`MemoryStore` instance as `cache_backend`) is as inconsistent across Puma workers and is now warned about too. The
|
|
1014
|
+
line reads "Using an in-process permission cache in production" (it said "in-process MemoryCache"): update a log
|
|
1015
|
+
alert that matches the old text. **Opt out:** use a store the workers share.
|
|
1016
|
+
<!-- pin: test/permission_forge/cache_warnings_test.rb#production gets the production warning, not the development one -->
|
|
1017
|
+
<!-- pin: test/permission_forge/cache_warnings_test.rb#production with a shared store does not warn -->
|
|
1018
|
+
- **What the cache tasks print is one outcome.** `rake permission_forge:cache:clear`, `erasure` and
|
|
1019
|
+
`cleanup_expired_assignments` print through `RakeOutput` (see Added): "Cached permissions removed: yes (the cache
|
|
1020
|
+
store confirmed it)" and a "Cache scope" line that says what the clear means (a shared store: every process sees it,
|
|
1021
|
+
each user rebuilds on their next request; a per-process store: this process only), instead of "Cache cleared: true",
|
|
1022
|
+
"invalidated 1 user(s)" and a "The cache is cold" line followed by the per-process note it contradicted. Labels
|
|
1023
|
+
changed ("Audit log name labels cleared as actor: 2", one key per line). **No opt-out:** output only; the result
|
|
1024
|
+
Hashes and events are additive. A script that matched `Cache cleared: true` matches `Cached permissions removed:`.
|
|
1025
|
+
<!-- pin: test/permission_forge/tasks/cache_clear_task_test.rb#with ACTOR and REASON: every holder is invalidated, the event names the actor from rake -->
|
|
1026
|
+
<!-- pin: test/permission_forge/tasks/rake_output_test.rb#an erasure on an in-memory store says the running servers kept the cached permissions, in output and event -->
|
|
1027
|
+
- **`DRY_RUN=1`, `yes` or `TRUE` now mean a dry run for `rake permission_forge:erasure` and `audit_events:purge`;
|
|
1028
|
+
only `false`, `0` or `no` executes.** The erasure took only the literal `true` as a dry run, so `DRY_RUN=1` (the
|
|
1029
|
+
purge's documented dry run) erased the person. Unset, each keeps its default: the erasure is a dry run, the purge
|
|
1030
|
+
deletes. **No opt-out:** a value other than an explicit false never deletes.
|
|
1031
|
+
<!-- pin: test/permission_forge/tasks/gdpr_erasure_task_test.rb#test DRY_RUN values other than an explicit false are a dry run -->
|
|
1032
|
+
- **Service results gained keys (additive).** `ConsentWithdrawalHandler.call` answers the same keys for a real run and
|
|
1033
|
+
a dry run (the real run answered `{ revoked_sets:, invalidated: }` only; it now also answers `affected_sets`,
|
|
1034
|
+
`affected_assignment_count` and `dry_run: false`, and a dry run also answers `invalidated: false`), and both answer
|
|
1035
|
+
`cache`; `ExpiredAssignmentCleaner.call` answers `affected_user_ids`, `permission_sets` and `cache`;
|
|
1036
|
+
`DataSubjectErasure.call` answers `permission_sets`, `groups`, `events_scrubbed` and `cache` (`cache_cleared` kept);
|
|
1037
|
+
`BulkPermissionSetAssignment.call` and `PermissionCacheClearer.call` answer `cache`. See
|
|
1038
|
+
[GDPR data handling](docs/gdpr-data-handling.md). **No opt-out:** nothing was removed or renamed.
|
|
1039
|
+
<!-- pin: test/permission_forge/services/consent_withdrawal_handler_test.rb#a real run answers the keys a dry run answers -->
|
|
1040
|
+
- **`rake permission_forge:audit` reports a per-process cache from the configured store.** `process_local_cache`
|
|
1041
|
+
(High, production) now asks the store the configuration builds (`cache_store.process_local?`), so `cache_backend =
|
|
1042
|
+
:rails_cache` over a `:memory_store` is reported too, not only `cache_backend = :memory`. **No opt-out:** a finding.
|
|
1043
|
+
<!-- pin: test/permission_forge/services/security_audit_test.rb#a process-local cache is flagged only in production -->
|
|
1044
|
+
- **`rails -T` descriptions.** `permission_forge:audit` no longer says "Read-only": it changes no permission data and
|
|
1045
|
+
records the run in the audit log (`report.generated`); the report's footer says "No permission data was changed"
|
|
1046
|
+
and that the run is recorded (it said "Read-only: nothing was changed"). `permission_forge:cache:clear` says what it
|
|
1047
|
+
does first; the upgrade hint for 0.13 installs comes last. **No opt-out:** text only.
|
|
1048
|
+
<!-- pin: test/permission_forge/tasks/rake_output_test.rb#the audit task says it changes no permission data and records the run -->
|
|
1049
|
+
<!-- pin: test/permission_forge/tasks/rake_output_test.rb#the cache:clear description leads with what it does, the upgrade hint last -->
|
|
1050
|
+
<!-- pin: test/permission_forge/services/security_audit_test.rb#report counts, threshold filtering and renderings -->
|
|
1051
|
+
- **The stale-assignment report and the admin agree on the threshold.** Without `THRESHOLD_DAYS` the rake task, and
|
|
1052
|
+
`Services::StaleAssignmentAuditor.call` without `threshold_days:`, now use `config.admin_stale_assignment_days`, as
|
|
1053
|
+
the Home Stale list, the person page and `rake permission_forge:audit` already did; they used a fixed 90 before.
|
|
1054
|
+
Nothing changes unless you set `admin_stale_assignment_days` to something other than 90. Callers that pass
|
|
1055
|
+
`threshold_days:` (the security audit does) are unaffected; `threshold_days: nil` now means the setting instead of
|
|
1056
|
+
raising. **Opt out:** run the task with `THRESHOLD_DAYS=90`, and call
|
|
1057
|
+
`Services::StaleAssignmentAuditor.call(threshold_days: 90)`.
|
|
1058
|
+
<!-- pin: test/permission_forge/services/stale_assignment_auditor_test.rb#the default threshold follows admin_stale_assignment_days -->
|
|
1059
|
+
<!-- pin: test/permission_forge/tasks/audit_rake_tasks_test.rb#stale_assignments without THRESHOLD_DAYS uses admin_stale_assignment_days -->
|
|
1060
|
+
- **`rake permission_forge:audit` refuses a `FAIL_ON` it does not know.** A typo counted as `info` and failed the build
|
|
1061
|
+
on any finding; the task now prints the levels on stderr and exits 2 before the review runs (as `stale_assignments`
|
|
1062
|
+
does with a bad `THRESHOLD_DAYS`). Levels are read in any case (`FAIL_ON=HIGH` is `high`). **No opt-out:** a valid
|
|
1063
|
+
level reads as before; `FAIL_ON=info` keeps failing on every finding.
|
|
1064
|
+
<!-- pin: test/permission_forge/tasks/rake_output_test.rb#an unknown FAIL_ON stops before the review with exit 2 and the levels -->
|
|
1065
|
+
<!-- pin: test/permission_forge/tasks/rake_output_test.rb#FAIL_ON is read in any case -->
|
|
1066
|
+
- **`rake permission_forge:audit_masks` names the Permission Set, the suggested strategy and what matched.** An
|
|
1067
|
+
unmasked field is reported once per set, and the lines looked like duplicates; "matches pattern 'first1_last4'"
|
|
1068
|
+
named the mask the pattern suggests, not the pattern. The lines now read `[UNMASKED] Customer.email (Permission Set:
|
|
1069
|
+
Support) — personal data, not masked (suggested strategy: 'email'; matches 'email')` and `[UNMAPPED] Customer.phone
|
|
1070
|
+
— personal data, no active ResourceField (suggested strategy: 'last4')` (plus `; matches '<pattern>'` or `; named by
|
|
1071
|
+
filter_parameters` where the finding records what matched); `unmasked_field` and `unmapped_column`
|
|
1072
|
+
findings gain a `pattern` key (the other keys are kept). A CI step that greps `[UNMASKED] Customer.name` still
|
|
1073
|
+
matches; one that matches the rest of the old line must change. **No opt-out:** match on `[UNMASKED]
|
|
1074
|
+
<Model>.<field>` (or read `Services::GdprFieldAuditor.call`'s Hash) instead of the whole line.
|
|
1075
|
+
<!-- pin: test/permission_forge/tasks/audit_masks_task_test.rb#prints one line per finding and exits 1 -->
|
|
1076
|
+
<!-- pin: test/permission_forge/services/gdpr_field_auditor_text_report_test.rb#keeps the start of the lines existing CI output already had, names the set and the suggested strategy -->
|
|
1077
|
+
- **`rule/unenforced_rules` leaves out Resources named after no model.** A Resource named `Custmer` was listed under
|
|
1078
|
+
`integrity/unknown_models` and, with its rules, under `rule/unenforced_rules` (and Home's record-rules card) — there
|
|
1079
|
+
is no query to scope. Only Resources whose model the app can load (`Admin::ModelCatalog#find`) are listed there now.
|
|
1080
|
+
**No opt-out:** `integrity/unknown_models` still names the Resource.
|
|
1081
|
+
<!-- pin: test/permission_forge/admin/services_test.rb#rules of a Resource named after no model the app can load are not flagged -->
|
|
1082
|
+
<!-- pin: test/permission_forge/services/security_audit_integrity_check_test.rb#a Resource named after a constant that is no model with a table is a Resource for a model this app does not have -->
|
|
1083
|
+
- **A missing column costs one metadata query per process, and a reset only after a real schema change.** The engine,
|
|
1084
|
+
the admin and the services read a missing name (a `_uuid` variant, a timestamp a model lacks, a declared virtual
|
|
1085
|
+
field) again through `SchemaRead` once per process: one metadata query, and a model reset only when the database's
|
|
1086
|
+
columns differ from Rails' (a migration while the process was up). Pages that list models (Sync, the rule
|
|
1087
|
+
builder) compare their columns with the database once per render; Home and the navigation read the schema cache. `FieldVariants` asks `FieldNames.declares?` plus
|
|
1088
|
+
`VirtualFields#include?` whether the model has a variant name. **No opt-out:** the alternative is the stale answer
|
|
1089
|
+
until the process restarts.
|
|
1090
|
+
<!-- pin: test/permission_forge/schema_freshness_test.rb#a missing name on an unchanged schema never resets the model -->
|
|
1091
|
+
<!-- pin: test/permission_forge/schema_freshness_test.rb#every reader sees a column a migration added after the schema was read, without a reset -->
|
|
1092
|
+
- **The JSON API answers JSON only.** `GET /api/permission_sets.xml` (any extension but `.json`) answered the JSON list
|
|
1093
|
+
with a 200; it now names no endpoint and gets the API's JSON 404 (`No API endpoint answers this path and method`).
|
|
1094
|
+
No extension and `.json` answer as before. **No opt-out:** the API has no other format.
|
|
1095
|
+
<!-- pin: test/permission_forge/api/unmatched_routes_test.rb#an extension other than .json reaches no endpoint: the JSON 404 envelope -->
|
|
1096
|
+
<!-- pin: test/permission_forge/api/unmatched_routes_test.rb#.json and no extension still answer -->
|
|
1097
|
+
- **Unrouted API calls answer the JSON 404 envelope.** Any other path or method under the API mount
|
|
1098
|
+
(`/api/unknown`, a `POST`, `PATCH` or `DELETE` on the read-only `audit_events`) answered Rails' HTML 404 page; it is
|
|
1099
|
+
now `{"error":{"status":404,"messages":"No API endpoint answers this path and method"}}`, after the same guards as
|
|
1100
|
+
every endpoint (a disabled API stays a bodiless 404, a refused `api_auth` a 401). **No opt-out:** an HTML page was no
|
|
1101
|
+
API answer.
|
|
1102
|
+
<!-- pin: test/permission_forge/api/unmatched_routes_test.rb#an unknown path or method under the API answers the JSON 404 envelope -->
|
|
1103
|
+
<!-- pin: test/permission_forge/api/unmatched_routes_test.rb#it runs api_auth first: an unauthenticated caller gets the 401, recorded like any API call -->
|
|
1104
|
+
- **Every API 422 has one shape: field-keyed messages.** Input the API cannot read answered `messages` as one string
|
|
1105
|
+
(`"expires_at is not an ISO 8601 date or time"`, `"param is missing or the value is empty or invalid:
|
|
1106
|
+
permission_set"`, `"Invalid action: …"`), while a failed validation answered an object. Both are now an object that
|
|
1107
|
+
maps the field to a list of messages, keyed by the request parameter (`{"expires_at": ["is not an ISO 8601 date or
|
|
1108
|
+
time"]}`, `{"permission_set": ["is missing, empty or not an object"]}`, `{"bulk_action": ["must be assign or
|
|
1109
|
+
revoke"]}`) or `base` when the error names none (`generate_fields`' failure). The messages follow the request's
|
|
1110
|
+
locale. 401, 403 and 404 keep their string. **No opt-out:** a client that read the string reads
|
|
1111
|
+
`messages.values.flatten` instead. See [`docs/admin-api.md`](docs/admin-api.md#response-format).
|
|
1112
|
+
<!-- pin: test/permission_forge/api/permission_sets_controller_test.rb#a section that is not an object is a field-keyed 422, like a validation error -->
|
|
1113
|
+
<!-- pin: test/permission_forge/api/permission_set_users_controller_test.rb#an expires_at that is not ISO 8601 is a 422 and assigns nobody, single and bulk -->
|
|
1114
|
+
- **An API `expires_at` given as a date means the end of that day.** `"2026-10-22"` on `POST …/users` and
|
|
1115
|
+
`POST …/users/bulk` is now 23:59:59.999999 of that day in `Time.zone`, as the admin's end-date presets and its custom
|
|
1116
|
+
date; it was the beginning of the day (00:00), so the assignment had ended when its last day started. A date and time
|
|
1117
|
+
(`2026-10-22T10:00`, with or without an offset) is read as before. **Opt out:** send the moment you mean,
|
|
1118
|
+
`2026-10-22T00:00`.
|
|
1119
|
+
<!-- pin: test/permission_forge/api/permission_set_users_controller_test.rb#a date alone is the end of that day, so today is accepted and lasts through it -->
|
|
1120
|
+
<!-- pin: test/permission_forge/api/permission_set_users_controller_test.rb#a date or a time without an offset is read in Time.zone -->
|
|
1121
|
+
- **The API refuses an `expires_at` that is not after now.** A single assignment or a bulk `assign` with an end that
|
|
1122
|
+
has passed (`"2020-01-01"`, yesterday, a minute ago) created an assignment that granted nothing, answered 201/200. It
|
|
1123
|
+
is now a 422 (`{"expires_at": ["must be in the future"]}`) and nobody is assigned. A bulk `revoke` ignores
|
|
1124
|
+
`expires_at`, also one that is not ISO 8601 (a revoke removes the assignments whatever their end); the admin's Undo
|
|
1125
|
+
still restores a removed assignment with the end it had. **No opt-out:** such an assignment granted nothing.
|
|
1126
|
+
<!-- pin: test/permission_forge/api/permission_set_users_controller_test.rb#an expires_at that is not in the future is a 422 and assigns nobody, single and bulk -->
|
|
1127
|
+
<!-- pin: test/permission_forge/api/permission_set_users_controller_test.rb#a bulk revoke ignores an expires_at it cannot read -->
|
|
1128
|
+
- **A refusing `api_auth` answers 401 Unauthorized instead of 403.** A wrong or missing token is a failed
|
|
1129
|
+
authentication: clients that refresh credentials on 401 did not on the 403. The body is
|
|
1130
|
+
`{"error":{"status":401,"messages":"Not authenticated"}}`, in the request's locale; the `api.auth_failed` event keeps
|
|
1131
|
+
its reason (`api_auth_denied`) and records `status: 401`. No `WWW-Authenticate` challenge is invented (the gem does
|
|
1132
|
+
not know your scheme): one your `api_auth` sets on `controller.response.headers` is kept. 403 now means an
|
|
1133
|
+
authenticated caller the authorization lambdas refuse, a lock, a missing tenant or `audit_log_authorization`.
|
|
1134
|
+
**No opt-out:** a client that treated 403 as "bad token" checks 401.
|
|
1135
|
+
<!-- pin: test/permission_forge/api/base_controller_test.rb#a WWW-Authenticate challenge set by api_auth is sent with the 401; none is invented -->
|
|
1136
|
+
<!-- pin: test/permission_forge/audit/admin_plane_events_test.rb#an API request api_auth refuses records api.auth_failed (401) with who the host knows -->
|
|
1137
|
+
- **A 403 for a missing authorization lambda says so.** While `api_require_authorization` is on and
|
|
1138
|
+
`api_action_authorization` / `api_assignment_authorization` is unset, a change or an assignment answered "Action not
|
|
1139
|
+
authorized" / "Assignment not authorized", the same text as a real denial. It now names the missing option ("API
|
|
1140
|
+
changes need config.api_action_authorization; …"); a lambda's refusal keeps its message. **No opt-out:** a message.
|
|
1141
|
+
<!-- pin: test/permission_forge/api/base_controller_test.rb#a mutation refused because an authorization lambda is unset says which option is missing -->
|
|
1142
|
+
- **`generate_fields` reads `grant_all` as a boolean.** Only the strings `"true"` and `"1"` granted: a JSON `true`
|
|
1143
|
+
(or `1`) answered 201 with fields without edit. `true`, `1` and the words `true`, `t`, `on`, `1` (any case) now
|
|
1144
|
+
grant; `false`, `0`, the words `false`, `f`, `off`, `0`, `null` and no `grant_all` do not — the one boolean reading
|
|
1145
|
+
of the admin and the API (`ParamShapes.boolean`); anything else (`"yes"`, `""`, `2`) is a 422
|
|
1146
|
+
`{"grant_all": ["must be true or false"]}` and creates no field. **No opt-out:** send one of the values above.
|
|
1147
|
+
<!-- pin: test/permission_forge/api/resources_controller_test.rb#POST generate_fields reads grant_all as a JSON boolean, 1 or their strings -->
|
|
1148
|
+
<!-- pin: test/permission_forge/api/resources_controller_test.rb#POST generate_fields refuses a grant_all it cannot read with a 422 keyed by grant_all -->
|
|
1149
|
+
- **A bulk assignment that names nobody is a 422.** `POST …/users/bulk` without `user_ids` (missing, `[]`, `""` or
|
|
1150
|
+
only blanks) answered 200 with nothing done; it is now `{"user_ids": ["must name at least one user"]}`, on `assign`
|
|
1151
|
+
and `revoke`, and records no event, like the other 422s. Ids that name nobody are still reported in `skipped`.
|
|
1152
|
+
**No opt-out:** such a request did nothing.
|
|
1153
|
+
<!-- pin: test/permission_forge/api/permission_set_users_controller_test.rb#a bulk request that names nobody is a 422 keyed by user_ids, recorded like the other 422s -->
|
|
1154
|
+
- **A bulk `user_ids` that is not a list is a 422.** `"user_ids": "1,3"` was read as one unknown id and answered 200
|
|
1155
|
+
`{"assigned": 0, "skipped": ["1,3"]}`; a string, a number or an object now answers
|
|
1156
|
+
`{"user_ids": ["must be a list of user ids"]}` and changes nothing. **No opt-out:** send a JSON list.
|
|
1157
|
+
<!-- pin: test/permission_forge/api/permission_set_users_controller_test.rb#a bulk user_ids that is not a list is a 422 keyed by user_ids and changes nothing -->
|
|
1158
|
+
- **API: a value of the wrong shape for its attribute is a 422 keyed by it, never dropped or cast.** Every attribute
|
|
1159
|
+
section (`permission_set`, `resource`, `resource_field`, `resource_action`, `permission_set_rule`,
|
|
1160
|
+
`permission_set_group`) is read through one shape table, `PermissionForge::Api::InputShapes` (each attribute's shape
|
|
1161
|
+
from its column; the activation conditions folded in), and every wrong attribute of the request is named. Strong
|
|
1162
|
+
parameters dropped a list or an object sent for a scalar with a log line (`PATCH {"is_active": [false]}` answered 200
|
|
1163
|
+
with the set still active: a deactivation or revocation that did nothing), and ActiveModel cast the rest: `"nope"`
|
|
1164
|
+
read as `true`, a JSON `true` for a name stored `"t"`, `1.5` for a description `"1.5"`. A boolean now takes `true`,
|
|
1165
|
+
`false`, `1`, `0` and the words `true`, `false`, `t`, `f`, `on`, `off`, `1`, `0` in any case (`""` is no boolean);
|
|
1166
|
+
text a String; `null` is passed on (a nullable column is cleared, a NOT NULL one keeps its own 422). A rule's
|
|
1167
|
+
`field_value` takes a JSON number or boolean (stored as its text, `5` → `"5"`) and, under `in`, `not_in` and
|
|
1168
|
+
`between`, a list stored joined with commas (`["active", "lead"]` → `"active,lead"`: it was a 422 "can't be blank");
|
|
1169
|
+
`permission_set_group_id` goes on taking the group's id or uuid, `null` or `""` clearing it. Outside a section, a
|
|
1170
|
+
copy's `name` that is not text (`…/clone`; it became the default name) and a bulk `user_ids` element that is no id (it
|
|
1171
|
+
was left out) — an object, a list, a fraction, a boolean — are 422s too; a blank string beside an id is ignored, as
|
|
1172
|
+
Rails' own `collection_singular_ids=` ignores it (a form built with `collection_check_boxes` sends one), and a JSON
|
|
1173
|
+
`null` in the list is removed by Rails' parameter parsing before the API reads it. The admin reads its flags the same way (the field drawer, a group, an action, the set
|
|
1174
|
+
editor): a crafted `group[is_active]=nope` switched the group on; it now goes back with "is_active must be true or
|
|
1175
|
+
false. Nothing was changed." A static guard keeps `.permit` out of the API controllers. **No opt-out:** send each
|
|
1176
|
+
attribute in its shape (docs/admin-api.md "Attribute shapes"); a dropped or miscast value was a silent no-op or an
|
|
1177
|
+
inverted flag.
|
|
1178
|
+
<!-- pin: test/permission_forge/api/input_shapes_test.rb#a list, an object, an unknown word, a fraction or "" for a boolean is a 422 keyed by it and changes nothing -->
|
|
1179
|
+
<!-- pin: test/permission_forge/api/input_shapes_test.rb#true, a number, a list or an object for a text attribute is a 422 keyed by it; nothing is renamed -->
|
|
1180
|
+
<!-- pin: test/permission_forge/api/input_shapes_test.rb#a list for field_value under in, not_in or between is joined with commas -->
|
|
1181
|
+
<!-- pin: test/permission_forge/api/input_shapes_test.rb#a copy name that is not text is a 422 keyed by name, and nothing is copied -->
|
|
1182
|
+
<!-- pin: test/permission_forge/api/input_shapes_test.rb#a bulk user_ids with an element that is no id is a 422 keyed by user_ids, and nobody is assigned -->
|
|
1183
|
+
<!-- pin: test/permission_forge/api/input_shapes_test.rb#a bulk user_ids holding a blank string beside an id ignores the blank, as Rails does -->
|
|
1184
|
+
<!-- pin: test/permission_forge/api/input_shapes_test.rb#no .permit in the API controllers outside pf_record_attributes -->
|
|
1185
|
+
<!-- pin: test/permission_forge/security/route_fuzz_test.rb#~answers a wrong shape of each section attribute with a 422 keyed by it -->
|
|
1186
|
+
<!-- pin: test/permission_forge/html/admin_boolean_params_test.rb#a group flag the admin cannot read is a flash, and the group is unchanged -->
|
|
1187
|
+
- **A single assignment or membership with an unusable `user_id` is a 422 keyed by `user_id`.**
|
|
1188
|
+
`POST …/permission_sets/:id/users` and `POST …/permission_set_groups/:id/users` answered 404 `"Record not found"`
|
|
1189
|
+
for a missing `user_id`, for one that is not an id (`"abc"`, `[2]`) and for one that names nobody, which read as if
|
|
1190
|
+
the set or the group were wrong. They now answer `{"user_id": ["is missing"]}`, `["is not a user id"]` or
|
|
1191
|
+
`["names nobody who can be assigned"]`, after the authorization lambdas, and assign nobody. An unknown id and
|
|
1192
|
+
someone `user_list_resolver` leaves out read the same, as in a bulk request's `skipped`; a 404 now always means the
|
|
1193
|
+
set or the group in the path. **No opt-out:** a client that treated that 404 as "no such person" reads the 422.
|
|
1194
|
+
<!-- pin: test/permission_forge/api/permission_set_users_controller_test.rb#a missing or malformed user_id is a 422 keyed by user_id, on a set that exists -->
|
|
1195
|
+
<!-- pin: test/permission_forge/api/permission_set_group_users_controller_test.rb#a missing, malformed or unknown user_id is a 422 keyed by user_id, on a group that exists -->
|
|
1196
|
+
- **An unknown `permission_set_group_id` is a 422 keyed by `permission_set_group_id`.** A value that names no group of
|
|
1197
|
+
the current tenant was a validation error under `permission_set_group`; the API now answers
|
|
1198
|
+
`{"permission_set_group_id": ["…"]}`, the parameter the request sent, before anything is saved. **No opt-out:** a
|
|
1199
|
+
client that read `messages.permission_set_group` reads `messages.permission_set_group_id`.
|
|
1200
|
+
<!-- pin: test/permission_forge/api/record_identifier_test.rb#a permission_set_group_id that names no group is a 422 keyed by it -->
|
|
1201
|
+
- **A request body the API cannot parse is a 400 in the API's envelope.** Invalid JSON sent as `application/json`
|
|
1202
|
+
answered Rails' own `{"status":400,"error":"Bad Request"}` (or raised where exceptions are not shown); it is now
|
|
1203
|
+
`{"error":{"status":400,"messages":"The request body could not be read. …"}}`, in the `Accept-Language` locale.
|
|
1204
|
+
Rails parses the body before `api_auth` runs, so this 400 can come before a 401; a disabled API stays a 404.
|
|
1205
|
+
**No opt-out:** the envelope is the API's.
|
|
1206
|
+
<!-- pin: test/permission_forge/api/base_controller_test.rb#a body that is not valid JSON is a 400 in the API envelope, in the Accept-Language locale -->
|
|
1207
|
+
<!-- pin: test/permission_forge/api/base_controller_test.rb#a body that is not valid JSON on a disabled API stays a bodiless 404 -->
|
|
1208
|
+
- **A `per_page` or `page` the API cannot read is the default, not 1.** `per_page=abc`, `0` or a negative number gave
|
|
1209
|
+
pages of one row; anything that is not a positive whole number is now `api_default_per_page` (a `page`, the first
|
|
1210
|
+
page). The `api_max_per_page` cap is unchanged. **No opt-out:** `meta.pagination` says what was used.
|
|
1211
|
+
<!-- pin: test/permission_forge/security/pagination_limits_test.rb#a per_page of 0, a negative one or one that is not a number is the default page size -->
|
|
1212
|
+
<!-- pin: test/permission_forge/security/pagination_limits_test.rb#a page that is not a positive whole number is the first page; the max clamp stays -->
|
|
1213
|
+
- **An API clone without a name is named as the admin's Duplicate names it.** `POST …/permission_sets/:id/clone`
|
|
1214
|
+
and `POST …/permission_set_groups/:id/clone` named the copy `"<name> (Copy)"` in English; they now use
|
|
1215
|
+
`permission_sets.copy_name`, `"<name> (copy)"` in the request's locale (`(kopje)`, `(копија)`), a set's copy
|
|
1216
|
+
numbered when taken, as the admin and the cloners do. **Opt out:** send `name`.
|
|
1217
|
+
<!-- pin: test/permission_forge/api/permission_sets_controller_test.rb#POST clone without a name names the copy as the admin does, numbered when taken, in the request locale -->
|
|
1218
|
+
- **`config.preset = :strict` sets `no_read_access_strategy = :deny`.** Under the strict preset a signed-in user
|
|
1219
|
+
whose Permission Sets do not grant Read on a model finds none of its records: `permission_scoped` and auto-scoping
|
|
1220
|
+
return no rows (associations and `belongs_to` presence checks included), `find_by` raises `NotAuthorizedError`.
|
|
1221
|
+
Give each role Read on the models its pages and validations load. The library default is unchanged. **Opt out:** `config.no_read_access_strategy = :rules_only`.
|
|
1222
|
+
<!-- pin: test/permission_forge/concerns/no_read_access_strategy_test.rb#the library default is :rules_only, the strict preset :deny -->
|
|
1223
|
+
<!-- pin: test/permission_forge/configuration_presets_test.rb#:strict closes exactly the README hardening checklist -->
|
|
1224
|
+
|
|
1225
|
+
- **One assignment's change says what the cache store confirmed, as a bulk revoke does.** Assigning, changing the end
|
|
1226
|
+
date, keeping, removing, restoring and undoing one person's assignment in the admin, and saving a batch of edits
|
|
1227
|
+
(Review and save, a field's drawer), were announced as done whatever the store answered; with the store down the
|
|
1228
|
+
toast is now an alert that adds "The cache store did not confirm that the cached permissions were cleared: the old
|
|
1229
|
+
permissions may stay in use until the cache lifetime ends." (`flash.cache_not_confirmed`, en/sq/mk), and on a
|
|
1230
|
+
per-process store every such toast ends with `flash.cache_process_local`. Group membership changes no cached
|
|
1231
|
+
permissions and reports none. **No opt-out:** a toast's wording; share the cache store to drop the per-process
|
|
1232
|
+
sentence.
|
|
1233
|
+
<!-- pin: test/permission_forge/html/assignment_cache_outcome_test.rb#removing one assignment with the store down is an alert, and still removes it -->
|
|
1234
|
+
<!-- pin: test/permission_forge/html/assignment_cache_outcome_test.rb#saving batched edits with the store down is an alert -->
|
|
1235
|
+
- **`POST` and `DELETE …/permission_sets/:uuid/users` answer the cache outcome.** The single grant answers
|
|
1236
|
+
`{ "data": {…}, "meta": { "cache": { "confirmed", "process_local", "in_request", "unconfirmed" } } }` (201), as the
|
|
1237
|
+
bulk endpoint answers `cache`; the single revoke answers 200 with the removed assignment and the same `meta.cache`
|
|
1238
|
+
(through 0.14.x a 204 without a body). Declared in `Api::Schema` (`single_with_cache`). **No opt-out:** a client
|
|
1239
|
+
that requires 204 must accept 200 (every 2xx is success); `meta` is additive.
|
|
1240
|
+
<!-- pin: test/permission_forge/api/assignment_cache_outcome_api_test.rb#a single revoke with the store down answers that nothing was confirmed -->
|
|
1241
|
+
<!-- pin: test/permission_forge/api/assignment_cache_outcome_api_test.rb#a single grant answers the cache outcome -->
|
|
1242
|
+
- **More warnings are SECURITY warnings.** A warning means access wider than configured, or a change unrecorded, and
|
|
1243
|
+
now says so (`[PermissionForge] SECURITY warning:`) for: a model loaded with another `tenant_model` (or none) than the
|
|
1244
|
+
configuration's; auto-applied masking not complete at boot, a masking token the store did not take, a store that
|
|
1245
|
+
fails the masking sync, and a sync that raised (it was a bare `logger.error`); the production per-process cache and
|
|
1246
|
+
the production masking that cannot be announced (their `WARNING:` prefix is gone); `database_row_level_security` on
|
|
1247
|
+
an adapter that is not PostgreSQL; the audit table that cannot be checked (a bare `logger.warn` before). A log alert
|
|
1248
|
+
that matched `WARNING: Using an in-process permission cache` matches `Using an in-process permission cache`.
|
|
1249
|
+
**No opt-out:** log text and severity; each warning names the setting that removes its condition.
|
|
1250
|
+
<!-- pin: test/permission_forge/model_bindings_test.rb#a tenant_model mismatch is a SECURITY warning; a user_class one is not -->
|
|
1251
|
+
<!-- pin: test/permission_forge/boot_warnings_channel_test.rb#every warning has its severity in SEVERITY, the one its site passes, and a reason when it is config -->
|
|
1252
|
+
- **Inside a caller's transaction, a consent withdrawal and the expired-assignment clean-up report a pending cache
|
|
1253
|
+
outcome.** They reported the outcome measured before the commit as confirmed, while the bulk assignment and the
|
|
1254
|
+
erasure reported `confirmed: nil`. "Deferred" has one reading now, `Cache::Report#deferred?` (a transaction was open
|
|
1255
|
+
when the work was measured): `cache[:confirmed]` and `invalidated` are `nil` there, as is
|
|
1256
|
+
`cleanup.expired_assignments`' `invalidated`. **No opt-out:** the answer is only known at the commit; `cache.settled`
|
|
1257
|
+
records it for the consent withdrawal.
|
|
1258
|
+
<!-- pin: test/permission_forge/services/cache_settlement_outcome_test.rb#inside a caller transaction a consent withdrawal reports deferred, not confirmed -->
|
|
1259
|
+
<!-- pin: test/permission_forge/services/cache_settlement_outcome_test.rb#inside a caller transaction the clean-up reports deferred, not confirmed -->
|
|
1260
|
+
- **The audit log's page reads like every page parameter.** A whole number past the last page (`page=99999…`, 40
|
|
1261
|
+
digits) is the last page, as `page=99` was; it was the first. **No opt-out:** a page that does not exist was never
|
|
1262
|
+
meant.
|
|
1263
|
+
<!-- pin: test/permission_forge/release_readiness/integer_param_guard_test.rb#the audit log reads its page with the one reader: a number past the last page is the last page -->
|
|
1264
|
+
|
|
1265
|
+
- **A rule comparing a value with an attribute declared with `encrypts` is refused, and a stored one is named.**
|
|
1266
|
+
Active Record encryption keeps the column's name and stores the ciphertext in it; a rule compares the column in raw
|
|
1267
|
+
SQL, so `phone equal '+1 555 0100'` compared the plaintext with the ciphertext — deterministic encryption included
|
|
1268
|
+
(Active Record encrypts a value only in `where(attribute: value)`) — and silently matched nothing (field test round 7,
|
|
1269
|
+
E#73). Every built-in operator that takes a value now matches nothing on such a column in the application layer and in
|
|
1270
|
+
the RLS policy (version 7 compiles it to `FALSE`), and `PermissionSetRule` refuses a new rule — or a change of its
|
|
1271
|
+
column, path, join type or resource, or of its operator off the null checks — on `field_name` ("names phone, an
|
|
1272
|
+
encrypted attribute of Customer: the database holds its ciphertext, so a rule can only check whether it is empty";
|
|
1273
|
+
en, sq, mk; the JSON API answers 422). `is_null` / `is_not_null` keep working: the ciphertext is NULL exactly when the
|
|
1274
|
+
value is. A rule saved before the host encrypted the attribute stays editable in its other attributes (a copy keeps
|
|
1275
|
+
it as it is), the audit lists it as `encrypted_rule_columns` (High while people hold the set) and Home under "Rules
|
|
1276
|
+
that match nothing" (`Operators::RuleProblem` kind `:encrypted_column`; one reader of the declarations,
|
|
1277
|
+
`PermissionForge::EncryptedAttributes`, a canary watches `encrypted_attributes`). The column is found wherever the
|
|
1278
|
+
rule reaches it: in any table its query joins — an alias (`twins_notes.body` on a self-join), a `has_many :through`'s
|
|
1279
|
+
join table, a table a default scope or a host's join brings in (each name the query gives a table is resolved to that
|
|
1280
|
+
table, `Operators::RuleSources.tables`, and to every loaded model on it, `EncryptedAttributes.models_on`; a name no
|
|
1281
|
+
loaded model is on counts as encrypted when any model along the rule's path encrypts a column of that name) — and
|
|
1282
|
+
under single-table inheritance (a column is encrypted when the
|
|
1283
|
+
class, its base class or any loaded class of the hierarchy on the same table encrypts it — a subclass's `encrypts`
|
|
1284
|
+
also refuses a rule on its parent, and Rails' own answer for the parent depends on load order; a class not loaded yet
|
|
1285
|
+
is not seen, so `rake permission_forge:audit`, which runs without eager loading, loads the application's models
|
|
1286
|
+
first, through the admin's model list's one definition, `Admin::ModelCatalog.eager_load_app_models`). Under
|
|
1287
|
+
`support_unencrypted_data` such a rule used to match the rows still stored in plaintext; it no longer does.
|
|
1288
|
+
**Opt out:** compare a deterministically encrypted attribute through a custom operator whose strategy calls
|
|
1289
|
+
`query.where(attribute => value)` (a custom operator gets the value as written).
|
|
1290
|
+
<!-- pin: test/permission_forge/models/permission_set_rule_encrypted_column_test.rb#a new rule comparing a value with an encrypted attribute is refused on field_name, with a message in every locale -->
|
|
1291
|
+
<!-- pin: test/permission_forge/models/permission_set_rule_encrypted_column_test.rb#a stored one matches nothing, is named, and stays editable in its other attributes -->
|
|
1292
|
+
<!-- pin: test/permission_forge/models/permission_set_rule_encrypted_column_test.rb#it is checked again when its column changes or its operator moves off the null checks -->
|
|
1293
|
+
<!-- pin: test/permission_forge/operators/rule_problem_taxonomy_test.rb#encrypted_column: a row written past the model is named, denies, is listed, compiles to FALSE -->
|
|
1294
|
+
<!-- pin: test/rls/rls_rule_parity_test.rb#a rule on an encrypted attribute: the null checks agree, every other built-in operator matches nothing -->
|
|
1295
|
+
<!-- pin: test/permission_forge/models/permission_set_rule_encrypted_reach_test.rb#a stored one through the alias matches nothing, never the row it should hide, and is reported -->
|
|
1296
|
+
<!-- pin: test/permission_forge/models/permission_set_rule_encrypted_reach_test.rb#an alias that cannot be resolved counts as encrypted when a model along the path encrypts the column -->
|
|
1297
|
+
<!-- pin: test/permission_forge/models/permission_set_rule_encrypted_reach_test.rb#a stored one on the join table matches nothing, never the row it should hide, and is reported -->
|
|
1298
|
+
<!-- pin: test/permission_forge/models/permission_set_rule_encrypted_reach_test.rb#a table a default scope joins is seen by the applier: the rule matches nothing -->
|
|
1299
|
+
<!-- pin: test/permission_forge/tasks/audit_rake_tasks_test.rb#the audit loads the application models before it reviews -->
|
|
1300
|
+
<!-- pin: test/permission_forge/models/permission_set_rule_encrypted_reach_test.rb#a rule on the base class is refused, and a stored one matches nothing for the base and the subclass -->
|
|
1301
|
+
- **The default `profile_builder` leaves out an attribute the user model encrypts.** The profile is cached with each
|
|
1302
|
+
person's permission hash, in the cache store (Redis, Solid Cache, a file or memory store). With `encrypts :email` (or
|
|
1303
|
+
`:name`) on the user model, the default builder stored the ciphertext JSON there before 0.15; reading it decrypted
|
|
1304
|
+
would put the plaintext in the cache store. `permissions_hash[:profile][:email]` and `[:name]` are now `nil` for an
|
|
1305
|
+
encrypted attribute (an encrypted `email` gives way to a plain `email_address`), and so is a name or email the model
|
|
1306
|
+
composes in a reader while it encrypts any attribute a name or email is read from (`name`, `full_name`,
|
|
1307
|
+
`display_name`, `first_name`/`last_name`, `firstname`/`lastname`, `email`, `email_address`: such a reader would hand
|
|
1308
|
+
over their plaintext; an encrypted attribute the label never reads is not looked at); the admin's labels, which are
|
|
1309
|
+
never cached, still read them decrypted (`Admin::PersonLabel`, `decrypt: false` for the profile). The generated
|
|
1310
|
+
initializer no longer sets `profile_builder`: the line is a commented example, which calls the public readers (they
|
|
1311
|
+
decrypt), so a new install uses this default; an initializer generated before keeps its own lambda, which does the
|
|
1312
|
+
same: remove the line to get the default. **Opt out:** set a `config.profile_builder` of your own that returns what the cache store may hold.
|
|
1313
|
+
<!-- pin: test/permission_forge/admin/encrypted_people_test.rb#the default profile_builder leaves encrypted attributes out; the admin label reads them decrypted -->
|
|
1314
|
+
<!-- pin: test/permission_forge/admin/encrypted_people_test.rb#under decrypt: false no reader is asked for a name or email while the model encrypts one the label reads -->
|
|
1315
|
+
<!-- pin: test/permission_forge/release_readiness/claims_regression_test.rb#CLAIMS-32 the generated initializer works without a Current class and keeps uuid in the profile -->
|
|
1316
|
+
- **A LIKE operator on a column that is not text is refused on `operator`, naming the column's type.** `seats
|
|
1317
|
+
contains '1'` on an integer column (PostgreSQL), or any LIKE operator on a `json` column, was refused as "is not a
|
|
1318
|
+
value Customer.seats can hold" on `field_value`, although the value was fine (field test round 7, D#22). It is now
|
|
1319
|
+
"matches text only, and Customer.seats is a column of type integer: use a condition that compares values" on
|
|
1320
|
+
`operator` (en, sq, mk; the JSON API's 422 is keyed `operator`), checked on the same occasions as a value; a stored one
|
|
1321
|
+
still matches nothing, and the audit lists it as `non_text_pattern_rules` (Medium while people hold the set) instead of
|
|
1322
|
+
`unusable_rule_values` (`Operators::ValueTyping.problem`, `RuleProblem` kind `:pattern_on_non_text`). **No opt-out:**
|
|
1323
|
+
such a rule never matched anything; a client that reads the 422 reads `operator`, and an alert on the finding's name
|
|
1324
|
+
uses the new one.
|
|
1325
|
+
<!-- pin: test/permission_forge/models/permission_set_rule_pattern_operator_test.rb#a LIKE operator on a column no pattern matches is refused on operator, naming the column type -->
|
|
1326
|
+
<!-- pin: test/permission_forge/api/generated_defaults_api_test.rb#POST create answers 422 on operator, naming the column type, for a LIKE operator on a column it cannot match -->
|
|
1327
|
+
<!-- pin: test/permission_forge/operators/postgres_column_types_test.rb#a LIKE operator on an integer, date or boolean column is refused on operator, naming the column type -->
|
|
1328
|
+
<!-- pin: test/permission_forge/operators/rule_problem_taxonomy_test.rb#pattern_on_non_text: a row written past the model is named, denies, is listed, compiles to FALSE -->
|
|
1329
|
+
- **Field and action rows the gem creates are labelled with their name, never with English made up from it.** The
|
|
1330
|
+
field generator, the admin's presets, Sync and bulk add, a row created without a label, and an action row added from
|
|
1331
|
+
a controller action stored `name.humanize` ("Billing email", "Created at"), which the Albanian and Macedonian admin
|
|
1332
|
+
then showed as if an admin had written it (field test round 7, A#127); a field label an admin blanks is stored as the
|
|
1333
|
+
name too (it went back to the humanized English). Rows already stored keep their label;
|
|
1334
|
+
`PermissionForge::GeneratedText` recognises a label equal to the name, to the humanized name or to "Untitled Field" as
|
|
1335
|
+
generated. `release_readiness/inflection_guard_test.rb` scans all of `lib/permission_forge` now. **Opt out:** write a
|
|
1336
|
+
label of your own (admin or API), or translate the column (`activerecord.attributes.<model>.<column>`).
|
|
1337
|
+
<!-- pin: test/permission_forge/services/resource_field_generator_test.rb#labels a generated row with its column name, never the humanized English -->
|
|
1338
|
+
<!-- pin: test/permission_forge/generated_text_test.rb#an action row made from a controller action key is labelled with the key -->
|
|
1339
|
+
<!-- pin: test/permission_forge/generated_text_test.rb#a field label an admin blanks is stored as the name -->
|
|
1340
|
+
<!-- pin: test/permission_forge/release_readiness/inflection_guard_test.rb#nothing in app/ or lib/permission_forge inflects English -->
|
|
1341
|
+
- **A Permission Set, a group or a rule created without a name — or renamed to a blank one — is refused, and a rule
|
|
1342
|
+
created without a blocked message stores none.** The install migration's English column defaults ("Untitled Permission Set", "Untitled
|
|
1343
|
+
Permission Set Group", "Untitled Rule", "You do not have permission to perform this action.") were stored by an API
|
|
1344
|
+
create or a create in code that sent none, and then shown in every language, returned by
|
|
1345
|
+
`NotAuthorizedError#failure_messages` and recorded as the audit's `record_label` (field test round 7, D#47). The models
|
|
1346
|
+
declare no default (`attribute :name, default: nil`, `attribute :failure_message, default: nil`), so the JSON API
|
|
1347
|
+
answers 422 on `name`. A name that is empty or only whitespace is refused the same way, in code, through the API and in
|
|
1348
|
+
the admin: every named model (Permission Set, group, rule, Resource, field row, action row) takes the one rule of
|
|
1349
|
+
`Concerns::RequiresName`, checked on create and on rename; a rule could be saved with name `""` or `" "` before
|
|
1350
|
+
(field test round 8), and a row stored so stays editable and shows as "Untitled …" in the admin; the install migration of a new install gives those columns no default (an upgraded install
|
|
1351
|
+
keeps its column defaults: the model's wins; see the migration guide). A rule whose stored message is the install
|
|
1352
|
+
default denies with the locale's sentence (`permission_forge.denials.rule_default`), read at denial time — the cached
|
|
1353
|
+
permission hash keeps the stored text. **Opt out:** send a name, and write a `failure_message` of your own.
|
|
1354
|
+
<!-- pin: test/permission_forge/generated_text_test.rb#a Permission Set, a group or a rule created without a name is refused, in every locale -->
|
|
1355
|
+
<!-- pin: test/permission_forge/api/generated_defaults_api_test.rb#POST create without a name answers 422 on name for a Permission Set, a group and a rule -->
|
|
1356
|
+
<!-- pin: test/permission_forge/models/requires_name_test.rb#a new record with a blank name is refused, with one message -->
|
|
1357
|
+
<!-- pin: test/permission_forge/api/generated_defaults_api_test.rb#a blank name is refused on create and on update for a Permission Set, a group and a rule -->
|
|
1358
|
+
<!-- pin: test/permission_forge/generated_text_test.rb#a rule created without a blocked message stores none, and denies with the sentence of the locale -->
|
|
1359
|
+
<!-- pin: test/permission_forge/generated_text_test.rb#a stored install default denies with the sentence of the current locale -->
|
|
1360
|
+
<!-- pin: test/permission_forge/generated_text_test.rb#the install migration gives no name of a set, group or rule and no blocked message an English default -->
|
|
1361
|
+
- **The audit log's own words are codes, shown in the page's language.** The reason the expired-assignment cleanup
|
|
1362
|
+
records when given none (`expired`, `Audit::ReasonCodes`) read "Arsyeja: expired" on an Albanian page (field test
|
|
1363
|
+
round 7, E#70); the event page, the log's rows and a person's audit tab render it from the locale
|
|
1364
|
+
(`pf_audit_reason`). An API change nobody names is stored with the actor label `api_client` (was "API client") and
|
|
1365
|
+
the settings observer's `boot` is a code too (`Audit::ActorCodes`); `Admin::ActorLabel` names both in the page's
|
|
1366
|
+
language (also an "API client" stored before), and so does the "who holds what" export's `granted_by` (a document
|
|
1367
|
+
people read; `granted_by_type` and `granted_by_id` stay as stored). The events export and the API return the stored
|
|
1368
|
+
values. **Opt out:** set `config.audit_actor_resolver`, so an API change names its client.
|
|
1369
|
+
<!-- pin: test/permission_forge/audit/generated_codes_test.rb#the cleaner records the reason code, and the event page, the log and the person tab render it translated -->
|
|
1370
|
+
<!-- pin: test/permission_forge/audit/generated_codes_test.rb#an API client nobody names and the boot are stored as codes and named in the locale -->
|
|
1371
|
+
<!-- pin: test/permission_forge/admin/audit_log_test.rb#who holds what names an actor stored as a code in the language of the export -->
|
|
1372
|
+
|
|
1373
|
+
- **A label the gem generated is shown as no label, and an English page keeps Rails' English.** A field or action row
|
|
1374
|
+
labelled with its own name, the name in English ("Created at", what the generator stored through 0.14) or "Untitled
|
|
1375
|
+
Field" read on a Macedonian or Albanian page as written — "Created at created_at" on the Fields tab (field test
|
|
1376
|
+
round 7, A#127) — and a column with a row and one without carried two names on one grid ("Api token" against
|
|
1377
|
+
`api_token`, B#26). `Admin::FieldLabel` and `Admin::ActionLabel` treat a generated label as none
|
|
1378
|
+
(`PermissionForge::GeneratedText.label?`) and fall back to the host's translation, then — only while `I18n.locale` is
|
|
1379
|
+
English — Rails' own English for the name (`human_attribute_name`: "Created at", what an English host saw before),
|
|
1380
|
+
then the name in `<code>`, shown once (the grid no longer reads "api_token api_token"); a label an admin wrote still
|
|
1381
|
+
wins. The field drawer, the action form, a set's and a group's name and the rule builder show generated text as a
|
|
1382
|
+
blank field with a translated placeholder (`Admin::Prefill.text`), and saving that blank back is no change: the
|
|
1383
|
+
stored text stays and no change or audit event is recorded (`Admin::Prefill.posted`, asked by every one of those
|
|
1384
|
+
saves; a set's and a group's name field is not `required` while it holds generated text, so a page under an
|
|
1385
|
+
install-default name saves any other change; typed text renames, and a blank over a label an admin wrote is stored
|
|
1386
|
+
as the name, as before). A blank sent for a description that holds nothing is no change either. A Permission Set,
|
|
1387
|
+
group or rule stored under an install-default English name ("Untitled Rule") is named in the page's language
|
|
1388
|
+
wherever the admin shows it (`Admin::RecordName`, `pf_record_name`; the locale-leak walk stores those defaults on the
|
|
1389
|
+
walked records) — the record lists of a Permission Set's and a person's Audit tab included (under
|
|
1390
|
+
`audit_trail_enabled`), whose CSV exports keep the stored values. **Opt out:** write a label or a name of your own, or
|
|
1391
|
+
translate the column (`activerecord.attributes.<model>.<column>`).
|
|
1392
|
+
<!-- pin: test/permission_forge/admin/generated_text_display_test.rb#a generated field label is no label: Rails English on an English page, the name verbatim elsewhere -->
|
|
1393
|
+
<!-- pin: test/permission_forge/html/generated_text_pages_test.rb#a generated label and no row read the same on a muting set, in every language (B#26) -->
|
|
1394
|
+
<!-- pin: test/permission_forge/html/generated_text_pages_test.rb#the field drawer pre-fills no generated label, and keeps an admin label -->
|
|
1395
|
+
<!-- pin: test/permission_forge/html/generated_text_unchanged_submit_test.rb#an unchanged field drawer keeps a generated label: no change, no event -->
|
|
1396
|
+
<!-- pin: test/permission_forge/html/generated_text_unchanged_submit_test.rb#a set under an install-default name: an unchanged submit changes nothing, a change saves -->
|
|
1397
|
+
<!-- pin: test/permission_forge/html/generated_text_unchanged_submit_test.rb#a rule under an install-default name and message: an unchanged builder save changes nothing -->
|
|
1398
|
+
<!-- pin: test/permission_forge/html/generated_text_pages_test.rb#the Permission Set Audit tab names generated text in the page language; its CSV keeps the stored values -->
|
|
1399
|
+
<!-- pin: test/permission_forge/admin/generated_text_display_test.rb#a set, group or rule stored under an install default name is named in the page language -->
|
|
1400
|
+
<!-- pin: test/permission_forge/html/locale_leak_test.rb#every screen shows only translated words and host data -->
|
|
1401
|
+
- **The rule builder's preview says what the save would say.** It runs the save path in memory — the builder's own
|
|
1402
|
+
checks, then the model's validations on the attributes the sentence shows — and shows the first refusal
|
|
1403
|
+
(`Admin::RulePreview`): an "is between" with one end typed read "a, is not a value status of Customer can hold"
|
|
1404
|
+
(field test round 7, B#87), and now reads "⚠ Will not save Enter both ends of the range.", as the save does; a rule
|
|
1405
|
+
the save would take shows its sentence. A preview before a value is entered names the missing value instead of
|
|
1406
|
+
"…". **No opt-out:** the preview only words what the save already refuses.
|
|
1407
|
+
<!-- pin: test/permission_forge/html/rule_preview_property_test.rb#the preview says the first error the save would show, or none when the save goes through -->
|
|
1408
|
+
<!-- pin: test/permission_forge/html/rule_preview_property_test.rb#B#87: an "is between" with one end says the range needs both ends -->
|
|
1409
|
+
- **The rule builder's checkboxes are read as every admin flag is.** `current_user`, `include_nulls` and `is_active`
|
|
1410
|
+
were compared with `"1"`, so `"true"` or `"on"` read as false; they go through `ParamShapes.flag` now (`true`, `on`,
|
|
1411
|
+
`1`, `t` in any case; an unchecked box keeps its default), and a value that is no boolean goes back with the
|
|
1412
|
+
`flash.invalid_param` message, nothing saved. **No opt-out:** the admin's own forms post `1` and `0`, which read as
|
|
1413
|
+
before.
|
|
1414
|
+
<!-- pin: test/permission_forge/html/rule_form_flags_test.rb#the builder reads "true", "on", "off" as the one boolean reading does -->
|
|
1415
|
+
<!-- pin: test/permission_forge/html/rule_form_flags_test.rb#a include_nulls the admin cannot read goes back with the flash, nothing saved -->
|
|
1416
|
+
- **Dry runs say "would", and answer that they did no cache work.** `rake permission_forge:audit_events:purge` with
|
|
1417
|
+
`DRY_RUN=1` printed "Would delete 3 audit event(s)" above "Audit events deleted: 3" (field test round 7, E#55); every
|
|
1418
|
+
label of `RakeOutput` that says a change was made has its dry-run form in the same table ("Audit events that would
|
|
1419
|
+
be deleted"). The dry runs of `ConsentWithdrawalHandler`, `DataSubjectErasure` and `ExpiredAssignmentCleaner` answer
|
|
1420
|
+
`cache: { confirmed: nil, ..., work: false }` (`Cache::Outcome.none`, `Cache::Report#none`; the consent dry run
|
|
1421
|
+
answered `confirmed: true`, the other two had no `cache`), and the rake line reads "none — no cache work was done".
|
|
1422
|
+
An outcome of real cache work keeps its four keys. **No opt-out:** a script that read a dry run's `cache[:confirmed]`
|
|
1423
|
+
reads `work`.
|
|
1424
|
+
<!-- pin: test/permission_forge/tasks/dry_run_output_test.rb#every label that says a change was made has its "would" form -->
|
|
1425
|
+
<!-- pin: test/permission_forge/tasks/dry_run_output_test.rb#the purge's dry run says would on every line -->
|
|
1426
|
+
<!-- pin: test/permission_forge/tasks/dry_run_output_test.rb#the consent withdrawal's dry run did no cache work, and says so -->
|
|
1427
|
+
<!-- pin: test/permission_forge/tasks/dry_run_output_test.rb#every dry run answers no cache work -->
|
|
1428
|
+
### Deprecated
|
|
1429
|
+
|
|
1430
|
+
- **An `api_auth` that sets `@admin = true` on the controller to open `GET /api/stats`.** Set
|
|
1431
|
+
`config.api_stats_authorization = ->(controller) { … }` instead. While that option is unset, `@admin` still allows,
|
|
1432
|
+
warning once per process through `PermissionForge.deprecator`; once it is set, `@admin` is ignored. Removed in 0.16.
|
|
1433
|
+
<!-- pin: test/permission_forge/api/dashboard_controller_test.rb#an @admin set by api_auth still allows the stats, with a deprecation warning -->
|
|
1434
|
+
<!-- pin: test/permission_forge/api/dashboard_controller_test.rb#with api_stats_authorization set, @admin no longer decides -->
|
|
1435
|
+
- **`PermissionForge::Admin::SensitiveColumns`** — use `PermissionForge::SensitiveColumns`; the old constant answers
|
|
1436
|
+
with the new class and warns through `PermissionForge.deprecator`. Removed in 0.16.
|
|
1437
|
+
<!-- pin: test/permission_forge/sensitive_columns_test.rb#PermissionForge::Admin::SensitiveColumns is a deprecated name for the same class -->
|
|
1438
|
+
- **`pf_time_ago`** (`ApplicationHelper`): it read Rails' `datetime.distance_in_words` keys, English without
|
|
1439
|
+
rails-i18n; it now answers as `pf_relative` and warns once through `PermissionForge.deprecator`. Removed in 0.16.
|
|
1440
|
+
<!-- pin: test/permission_forge/helpers/application_helper_test.rb#pf_time_ago is deprecated and reads as pf_relative -->
|
|
1441
|
+
|
|
1442
|
+
### Removed
|
|
1443
|
+
|
|
1444
|
+
- **`PermissionForge::Engine.apply_masked_attributes_to_managed_models`**, deprecated in 0.14: use
|
|
1445
|
+
`PermissionForge::MaskedModels.apply(only:)`.
|
|
1446
|
+
<!-- pin: test/permission_forge/engine_boot_paths_test.rb#the engine carries no boot, masking or warning logic, and nothing reaches into it -->
|
|
1447
|
+
- **The removal of 0.13's cache keys.** Through 0.14 every invalidation also removed the bare keys 0.13 wrote
|
|
1448
|
+
(`user_<id>`, `tenant_<t>_user_<id>`), so that a rolling deploy from 0.13 reached both versions' entries;
|
|
1449
|
+
`Cache::UserKeys::PREVIOUS_ENTRY_NAMESPACES` is now empty and an invalidation removes this version's (`v2:`) entries
|
|
1450
|
+
only: a revocation made by a 0.15 process no longer reaches what a 0.13 process has cached, which keeps serving it
|
|
1451
|
+
until `cache_ttl`. An application still on 0.13 follows
|
|
1452
|
+
[From 0.13](https://github.com/fetijashari/permission_forge/blob/main/docs/migration-guide.md#from-013), which states
|
|
1453
|
+
both directions and its two paths (through 0.14, with no such window, or straight to 0.15, accepting it).
|
|
1454
|
+
<!-- pin: test/permission_forge/cache/user_keys_test.rb#an invalidation names the stamp and the v2 keys, and no bare key of 0.13 -->
|
|
1455
|
+
<!-- pin: test/permission_forge/cache/user_keys_test.rb#the entries live under one namespace, the previous ones are gone -->
|
|
1456
|
+
<!-- pin: test/permission_forge/docs/upgrade_notes_test.rb#From 0.13 states the cache gap in both directions and the two paths -->
|
|
1457
|
+
|
|
1458
|
+
### Fixed
|
|
1459
|
+
|
|
1460
|
+
- **A session flag with a control character is a condition nobody can judge.** `requires_session_flag` with a newline,
|
|
1461
|
+
a tab or a NUL byte is refused on save; a stored one makes a muting set apply to every request and a granting set grant
|
|
1462
|
+
nothing (`ActivationConditions#session_flag_broken?`), and `rake permission_forge:audit` and Home list it with the other
|
|
1463
|
+
broken conditions. <!-- pin: test/permission_forge/models/permission_set_condition_rows_test.rb#a session flag with a control character is refused, and a stored one cannot be judged -->
|
|
1464
|
+
- **A NUL byte in any text attribute of the permission models is a validation error.** A save from code (the console,
|
|
1465
|
+
a host's service) with one raised — PostgreSQL's driver refuses any statement carrying it, the uniqueness checks
|
|
1466
|
+
included, and SQLite on Rails 7.1 reads the statement cut short; the admin and the API already dropped NUL bytes from
|
|
1467
|
+
request parameters. A value stored before keeps the row's other attributes editable.
|
|
1468
|
+
<!-- pin: test/permission_forge/concerns/validates_column_lengths_test.rb#a NUL byte in a string being written is a validation error, before any validator queries with it -->
|
|
1469
|
+
- **The rule builder's live sentence keeps up with typing**: a newer input aborts the request before it, so a late
|
|
1470
|
+
answer no longer overwrites the newer sentence, and a redirect (a lapsed session) or a whole page is never put into
|
|
1471
|
+
the panel.
|
|
1472
|
+
<!-- pin: test/permission_forge/html/admin_script_hygiene_test.rb#the rule preview aborts a stale request and injects fragments only -->
|
|
1473
|
+
- **The dependency validation names the prerequisite by its label** ("requires Read to be enabled", in the request's
|
|
1474
|
+
language) instead of the column (`read_access`); sq and mk no longer say "Permission Set", "commit and deploy" or
|
|
1475
|
+
"fail-open" in English, and the Effective access action pills word their state, not only a glyph.
|
|
1476
|
+
<!-- pin: test/permission_forge/models/resource_test.rb#the dependency message names the prerequisite by its label in every locale -->
|
|
1477
|
+
<!-- pin: test/permission_forge/i18n_test.rb#the left-behind lint bites -->
|
|
1478
|
+
<!-- pin: test/permission_forge/html/state_rendering_test.rb#an action pill on Effective access words its state, not only its glyph -->
|
|
1479
|
+
- **A toast no longer covers the sidebar's language and theme switch at desktop widths** (1024px): toasts centre over
|
|
1480
|
+
the main column (`--pf-sidebar-width`). The browser layout check now also runs at 390 and 1024px.
|
|
1481
|
+
<!-- pin: test/permission_forge/html/layout_lint_test.rb#every scroll container in the stylesheet is a containing block, and the frame paints its cue -->
|
|
1482
|
+
- **"Review and save" titled the browser tab with a raw value** (`{one: "Review %{count} change", …}`) in every locale:
|
|
1483
|
+
the page title now counts the changes. Every admin template is now rendered by the whole-UI walks (the reviews,
|
|
1484
|
+
previews, Home cards, the audit log off or refused), so their lints cover it.
|
|
1485
|
+
<!-- pin: test/permission_forge/html/admin_page_paths_test.rb#every POST answered with a page renders it -->
|
|
1486
|
+
<!-- pin: test/permission_forge/html/admin_page_paths_test.rb#every template under app/views is rendered by a walked screen -->
|
|
1487
|
+
- **A stored rule never raises at read time.** A rule row with an operator this version does not know (`regex`) raised
|
|
1488
|
+
`ArgumentError`, one with a join type that is none (`inner`) raised `ConfigurationError` — from `permission_scoped`,
|
|
1489
|
+
`find_by`, `find_by!`, and under `auto_query_scoping = :filter_only` from `Customer.count` and every other query —
|
|
1490
|
+
a 500 for every holder of the set. Such a rule matches nothing: `permission_scoped` is empty, `find_by` denies with
|
|
1491
|
+
`NotAuthorizedError`. A registry injected as a Hash or as an object answering `fetch(name)` is asked the same way.
|
|
1492
|
+
<!-- pin: test/permission_forge/services/permission_rule_applier_test.rb#the holder of such rules is denied every record, in every read path, never a 500 -->
|
|
1493
|
+
- **A rule naming a missing column denies instead of raising.** A rule whose field name is not a column of its table
|
|
1494
|
+
(`onwer_id` on `Customer`, written past the model, or a column renamed since) reached the database, so every
|
|
1495
|
+
`find_by` / `find_by!` / `permission_scoped` (and every query under `auto_query_scoping`) of each holder of its
|
|
1496
|
+
Permission Set raised `ActiveRecord::StatementInvalid` — a 500 in the host app. Under every built-in operator it now
|
|
1497
|
+
matches nothing, like a value the column cannot hold: its holders are denied every record of the resource. A custom
|
|
1498
|
+
operator's rule is still left to the database.
|
|
1499
|
+
An RLS policy compiles such a rule to `FALSE` (the sync failed on it; see `GENERATOR_VERSION` 7 under Changed). A new
|
|
1500
|
+
such rule is refused on save (see Changed). See
|
|
1501
|
+
[Using the wrong field name](docs/row-level-rules.md#using-the-wrong-field-name).
|
|
1502
|
+
<!-- pin: test/permission_forge/services/rule_value_typing_test.rb#a column the table does not have matches nothing, under every built-in operator -->
|
|
1503
|
+
<!-- pin: test/permission_forge/services/rule_value_typing_test.rb#a table that is none of the rule's matches nothing; a custom operator is still left to the database -->
|
|
1504
|
+
<!-- pin: test/permission_forge/services/rls_operator_sql_test.rb#a column the table does not have compiles to FALSE, whatever the operator or value -->
|
|
1505
|
+
- **Duplicating a set no longer fails on a row stored before today's checks** (an unknown operator or join type, a
|
|
1506
|
+
mask strategy that is not one, an IP entry that is no address): the copy keeps the stored values and behaves like
|
|
1507
|
+
its original; whatever a copy still cannot hold is a flash naming the row, never a 500 (admin Duplicate, "Start
|
|
1508
|
+
from", group Duplicate, API `clone`).
|
|
1509
|
+
<!-- pin: test/permission_forge/services/copy_stored_values_test.rb#admin_duplicate copies a set holding a stored rule_operator and keeps it -->
|
|
1510
|
+
<!-- pin: test/permission_forge/services/copy_stored_values_test.rb#a copy the validations still refuse is a flash naming the row, in the admin -->
|
|
1511
|
+
- **A join type this version does not know is applied by the default scope too** (it matches nothing); skipped there,
|
|
1512
|
+
it left `auto_query_scoping` wider than `find_by` and `permission_scoped`. A stored rule whose field name holds a
|
|
1513
|
+
control character can be edited (switched off). An IPv4-mapped request address (`::ffff:10.0.0.5`) matches IPv4
|
|
1514
|
+
entries. `EditableLock.permits?` answers for a group membership.
|
|
1515
|
+
<!-- pin: test/permission_forge/security/rule_join_type_test.rb#a blank join_type is a filter rule to the applier, the default scope and the model alike -->
|
|
1516
|
+
<!-- pin: test/permission_forge/models/permission_set_rule_validation_test.rb#a stored field name the format refuses stays editable in its other attributes, refused when it changes -->
|
|
1517
|
+
<!-- pin: test/permission_forge/services/permission_set_condition_evaluator_test.rb#an IPv4-mapped IPv6 request address matches the IPv4 entries -->
|
|
1518
|
+
<!-- pin: test/permission_forge/admin/editable_lock_test.rb#permits? answers for a group membership: refused both ways while the group is locked -->
|
|
1519
|
+
- **A rule whose association path cannot be joined denies instead of raising.** A join rule whose
|
|
1520
|
+
`field_accessor_path` names no association (`ownr` instead of `owner`, saved through the API with a 201 before this
|
|
1521
|
+
release) made every `find_by` / `find_by!` / `permission_scoped` of each holder of its Permission Set raise
|
|
1522
|
+
`ActiveRecord::ConfigurationError` ("Can't join 'Customer' to association named 'ownr'") — a 500 in the host app —
|
|
1523
|
+
and the audit did not list it. Such a rule — and one through a polymorphic `belongs_to`, a record-dependent scope
|
|
1524
|
+
(also one reached along a `:through`, which raised `NoMethodError` when Rails built the join), a broken `:through` /
|
|
1525
|
+
`inverse_of` or a class that cannot be loaded — now matches nothing under every operator: the join is never built.
|
|
1526
|
+
Paths ActiveRecord joins are unchanged. An RLS policy compiles such a rule to `FALSE` (see Changed).
|
|
1527
|
+
<!-- pin: test/permission_forge/models/permission_set_rule_path_test.rb#a holder of a rule whose path cannot be followed is denied every record, never a 500 -->
|
|
1528
|
+
<!-- pin: test/permission_forge/security/rule_join_type_test.rb#an association path that cannot be followed matches nothing -->
|
|
1529
|
+
<!-- pin: test/permission_forge/operators/rule_path_test.rb#every path ActiveRecord raises for is broken, at the segment it raises for -->
|
|
1530
|
+
- **A rule value with a NUL byte, or a field name with a control character, matches nothing.** A `field_value`
|
|
1531
|
+
carrying a NUL byte (a row written past the model; the request layer drops NUL bytes from parameters) was quoted
|
|
1532
|
+
into the statement, which the database read cut short — `ActiveRecord::StatementInvalid` for every holder. It is
|
|
1533
|
+
now unusable under every operator, built-in or custom (`Operators::ValueTyping.unquotable?`): the rule matches
|
|
1534
|
+
nothing, the audit lists it (`unusable_rule_values`) and Home names it; a new or changed value with a NUL byte is
|
|
1535
|
+
refused ("must not contain a NUL byte"; a stored one leaves the row editable). A `field_name` with a control
|
|
1536
|
+
character (a newline, a NUL byte) in a segment is no name (`Operators::RuleColumn.name?`): it matches nothing under
|
|
1537
|
+
a custom operator too, where a missing column used to reach the database as written.
|
|
1538
|
+
<!-- pin: test/permission_forge/services/permission_rule_applier_test.rb#a value with a NUL byte, or a field name with a control character, matches nothing -->
|
|
1539
|
+
- **The admin no longer draws a rule that matches nothing as a working rule, and shows what it names as stored.** A
|
|
1540
|
+
rule naming a missing column read "Only show Customers where Stauts is active, or Stauts is empty" on the Rules tab.
|
|
1541
|
+
The Rules tab, the delete confirmation, the field page, the rule preview, a person's effective access (and its
|
|
1542
|
+
Records cell, which now summarises such a rule first) and the Permission Set's records summary now say "⊘ Matches
|
|
1543
|
+
nothing" and why ("`stauts` is not a column of Customer", "`ownr` is not a relationship of Customer") with the rule
|
|
1544
|
+
as stored, never "or empty"; the rule's edit page opens with a banner saying so (a rule written with
|
|
1545
|
+
`update_column` is still `valid?` — the checks run when what it names changes — so the page shows what the row
|
|
1546
|
+
holds) (en, sq, mk). A missing name is shown as code as stored (it read "Onwer" and "Ownr" like real names); a
|
|
1547
|
+
relationship that exists but cannot be followed keeps its label.
|
|
1548
|
+
<!-- pin: test/permission_forge/html/broken_rules_admin_test.rb#the Rules tab says the rule matches nothing, and never "or empty" -->
|
|
1549
|
+
<!-- pin: test/permission_forge/html/broken_rules_admin_test.rb#the edit page of such a rule says so, as stored -->
|
|
1550
|
+
<!-- pin: test/permission_forge/html/field_test_round_five_admin_ui_test.rb#a broken name is shown as stored, in code, wherever the problem is said -->
|
|
1551
|
+
- **An empty step in a rule's association path no longer breaks the admin.** A path like `parent..owner` (written past
|
|
1552
|
+
the model) made Home and the Rules tab raise (`human_attribute_name('')`); it reads "The relationship path has an
|
|
1553
|
+
empty step (two dots in a row, or a dot at the start)", and saving one says "has an empty association name".
|
|
1554
|
+
<!-- pin: test/permission_forge/html/broken_rules_admin_test.rb#Home says why a relationship cannot be followed, and names an empty step of the path -->
|
|
1555
|
+
<!-- pin: test/permission_forge/models/permission_set_rule_path_test.rb#each cause has its own message -->
|
|
1556
|
+
- **A model file that raises while it loads no longer breaks Home or the Rules tab.** `Operators::RuleProblem` and
|
|
1557
|
+
`Operators::RuleColumn.for_rule` rescued only `NameError`, `ArgumentError` and `ActiveRecordError`: a model (or an
|
|
1558
|
+
association's class) whose file raised while it loaded made every page that describes rules a 500. A `ScriptError`
|
|
1559
|
+
(`SyntaxError`, `LoadError`) there, or any error while the rule's own model loads, is now "cannot tell" for those
|
|
1560
|
+
callers (no problem shown, the save not refused); a `StandardError` while an association's class loads makes the
|
|
1561
|
+
path broken ("… whose class cannot be loaded": shown, refused on save, matches nothing). The applier never answers
|
|
1562
|
+
for a model it cannot load: a `ScriptError` propagates from the query as before.
|
|
1563
|
+
<!-- pin: test/permission_forge/models/permission_set_rule_path_test.rb#a model file that raises while it loads is "cannot tell" for the admin, never an answer from the applier -->
|
|
1564
|
+
- **A field masked with a strategy this app does not know never raises.** `Services::MaskValue.call` raised
|
|
1565
|
+
`ArgumentError` for a strategy it did not know — from the model's masked getter, `attributes`, `as_json`,
|
|
1566
|
+
`FiltersResponseAttributes`, the admin's Fields tab — for a row written past the model, a blank strategy, or a custom
|
|
1567
|
+
strategy removed from `config.custom_mask_strategies` after rows named it. It masks fully (`********`) and reports
|
|
1568
|
+
the name once per process.
|
|
1569
|
+
<!-- pin: test/permission_forge/services/mask_value_test.rb#call masks fully with a strategy it does not know, blank included, and never raises -->
|
|
1570
|
+
- **The field drawer no longer rewrites a mask strategy this app does not know to `full`.** Saving another change
|
|
1571
|
+
(a label) saved `full`, audited as a strategy change, and the drawer showed "Masked (emial)" with no option
|
|
1572
|
+
selected. It now flags the strategy ("Unknown mask strategy — masked fully until fixed") and offers the stored name,
|
|
1573
|
+
selected and marked unknown, so a save that changes something else keeps it; only picking a strategy changes it.
|
|
1574
|
+
<!-- pin: test/permission_forge/html/broken_rules_admin_test.rb#saving the drawer with another change keeps a strategy this app does not know; picking one changes it -->
|
|
1575
|
+
<!-- pin: test/permission_forge/html/broken_rules_admin_test.rb#a field whose mask strategy this app does not know is drawn masked, with a badge, never a 500 -->
|
|
1576
|
+
- **A Resource named after a constant that is no model** (`Kernel`, `String`, a model without its table) made
|
|
1577
|
+
`ResourceFieldGenerator`, `ResourceWithFieldsBuilder` (the admin's "Add resource" preset path) and the API's
|
|
1578
|
+
`generate_fields` raise `NoMethodError`. The generator creates no fields and logs one line; the API answers 422.
|
|
1579
|
+
<!-- pin: test/permission_forge/services/resource_field_generator_test.rb#returns empty array when resource name is not a valid class -->
|
|
1580
|
+
- **API: an activation condition is never dropped silently.** `POST`/`PATCH /api/permission_sets` permitted
|
|
1581
|
+
`allowed_ips` as a scalar only, so a JSON array was dropped without a word (201 with `allowed_ips: null`, 200
|
|
1582
|
+
changing nothing) and an "office only" granting set applied from any IP. `allowed_ips` now takes a String (commas,
|
|
1583
|
+
newlines or spaces between entries) or an Array of Strings, normalised by the one parser; hours take an integer (or a
|
|
1584
|
+
String of digits); a session flag a String; `null` clears. Any other shape (an object, a boolean, a number for a
|
|
1585
|
+
list, an array for a flag or an hour) is a 422 keyed by the attribute, as is a list with no entry (`[]`, `[""]`).
|
|
1586
|
+
<!-- pin: test/permission_forge/api/permission_set_conditions_api_test.rb#the recorded scenario: an office-only granting set written as an array grants from the office only -->
|
|
1587
|
+
<!-- pin: test/permission_forge/api/permission_set_conditions_api_test.rb#an IP list with no entry is a 422 saying null clears the condition -->
|
|
1588
|
+
- **A uniqueness race is no longer a 500.** Two requests that create the same assignment, group membership, Resource
|
|
1589
|
+
or field at the same moment both pass the uniqueness validation, and the unique index refuses the second INSERT
|
|
1590
|
+
(`ActiveRecord::RecordNotUnique`), which nothing rescued. The API now answers it with the validation's own 422,
|
|
1591
|
+
keyed by the attribute (`{"user_id": ["already holds this Permission Set"]}`, `{"name": ["already exists in this
|
|
1592
|
+
permission set"]}`); the admin goes back to the page the form was on (303, never another host; its home page
|
|
1593
|
+
without a Referer) with the same message in an alert. `PermissionForge::UniqueConflict` names the validation from
|
|
1594
|
+
the index the adapter's error names and parses nothing else; an index that belongs to no validation of the gem's
|
|
1595
|
+
models is a 422 on `base` / a generic alert.
|
|
1596
|
+
<!-- pin: test/permission_forge/security/route_fuzz_test.rb#duplicate insert: POST /api/permission_sets/:set/users is the validation 422 -->
|
|
1597
|
+
<!-- pin: test/permission_forge/security/route_fuzz_test.rb#duplicate insert: POST /assignments goes back to the form with the message -->
|
|
1598
|
+
- **A column a migration added while the process was up was invisible**: a rule on it matched nothing at run time
|
|
1599
|
+
(`RuleColumn.find`), "Sync fields" and the field generator did not create its row, the Fields tab and the rule
|
|
1600
|
+
builder did not offer it and flagged a configured row on it "missing", a new `updated_at` was not made read-only, a
|
|
1601
|
+
`_uuid` variant got no entry in the permission hash, a declared virtual field that became a column stayed
|
|
1602
|
+
"virtual", the GDPR audit skipped it and the RLS generator left the tenant predicate out. Each reads it now without
|
|
1603
|
+
a restart (`SchemaRead`). A removed column leaves the process at its next comparing read (the admin's Resource page,
|
|
1604
|
+
Sync fields, the audit). A table a migration created while the process was up is found too (`data_source_exists?`
|
|
1605
|
+
caches a negative answer; `SchemaRead.table?` asks again at most every 30 s).
|
|
1606
|
+
<!-- pin: test/permission_forge/schema_freshness_test.rb#every reader sees a column a migration added after the schema was read, without a reset -->
|
|
1607
|
+
<!-- pin: test/permission_forge/schema_freshness_test.rb#a table created after the schema cache said it is not there is found -->
|
|
1608
|
+
- **A response key two fields map to reads by the stricter of them.** With custom `field_variant_mappings` that send
|
|
1609
|
+
two fields to one name (`supplier_id` and `supplier_ref` both to `supplier_key`) and a model without that name,
|
|
1610
|
+
`FiltersResponseAttributes` read a serializer's `supplier_key` by the first field's entry, so a hidden second field
|
|
1611
|
+
left the key readable. It now reads by the strictest of the fields' entries: read only if every one reads, edit only
|
|
1612
|
+
if every one edits, masked if any masks (with that field's strategy). The default mappings never send two fields to
|
|
1613
|
+
one name.
|
|
1614
|
+
<!-- pin: test/permission_forge/field_variants_test.rb#a response key two fields map to is hidden when either field is -->
|
|
1615
|
+
<!-- pin: test/permission_forge/field_variants_test.rb#a variant two fields map to gets the strictest of their entries -->
|
|
1616
|
+
- **The admin's effective-access explanation follows the engine on explicit-over-derived variant rows** (`variant_of`
|
|
1617
|
+
is nil for an explicit row).
|
|
1618
|
+
<!-- pin: test/permission_forge/admin/effective_access_test.rb#an explicit Hidden row for a variant name is hidden, in the engine and here -->
|
|
1619
|
+
- **A person's effective access composes single-table inheritance the way the engine does.** The view of
|
|
1620
|
+
`VipCustomer` showed the subclass entry's own fields and rules, while the engine reads the strictest field entry
|
|
1621
|
+
along the chain (`ResourceChain.fields`) and the rules of every entry: a field the base masks read as Editable, and a
|
|
1622
|
+
base entry without Read (which hides every field) was not shown at all. A subclass entry now lists the rules of its
|
|
1623
|
+
base entries too (a View All on the subclass skips only its own rules), its fields are composed along the chain, the
|
|
1624
|
+
resource flags follow `PermissionHashBuilder.granted_flags` (edit and delete only with read, Modify All only with
|
|
1625
|
+
View All), the Why panel says which Resource decided ("Decided by Customer, which this inherits from …", "Hidden: it
|
|
1626
|
+
inherits from Customer, which doesn't allow Read."), and a field drawer's "In other sets" / "Explain for" reach the
|
|
1627
|
+
base Resources a subclass answers to.
|
|
1628
|
+
<!-- pin: test/permission_forge/admin/effective_access_test.rb#a subclass entry lists the base entry's rules too, and View All on the subclass skips only its own -->
|
|
1629
|
+
<!-- pin: test/permission_forge/admin/effective_access_test.rb#a subclass shows its fields as the chain composes them: the strictest entry decides -->
|
|
1630
|
+
- **Effective access: the Why panel credits a set only with what it did to the entry.** A Muting Permission Set that
|
|
1631
|
+
masks or hides a field no set shows made the panel read "⊘ Hidden by muting" and "Contractors forces this field
|
|
1632
|
+
masked." for a field that was hidden because nobody granted it; a muting row that takes editing away was listed for
|
|
1633
|
+
a field nobody could edit; with two mutes the weaker one was named first; two masking sets were listed in an
|
|
1634
|
+
unstable order, so the first strategy named could differ from the one applied; a mask on a field already masked, or
|
|
1635
|
+
a mute on a subclass field its base's grants already hide, was credited too. The badge and the first reason now
|
|
1636
|
+
always word the entry's level (and strategy).
|
|
1637
|
+
<!-- pin: test/permission_forge/html/admin_view_models_test.rb#every WHY level word equals the level the hash entry gives, for every state -->
|
|
1638
|
+
- **Effective access says how each Permission Set shows a field.** The WHY panel of a field said "✓ Sales rep shows
|
|
1639
|
+
this field." when Sales rep shows it masked. It now reads by level: "shows this field masked (Email)", "shows this
|
|
1640
|
+
field read-only", "shows this field and lets people edit it", and for Muting Permission Sets "hides this field" or
|
|
1641
|
+
"forces this field masked" (`Admin::EffectiveAccess::FieldSource`, `FieldOutcome#grants` / `#mutes`).
|
|
1642
|
+
<!-- pin: test/permission_forge/html/field_test_round_two_copy_test.rb#the WHY panel says how each set shows the field -->
|
|
1643
|
+
<!-- pin: test/permission_forge/html/field_test_round_two_copy_test.rb#a muting set that forces a mask says so, in every locale -->
|
|
1644
|
+
- **A field hidden by a Muting Permission Set reads "⊘ Hidden by muting" everywhere.** The field's row said so, but its
|
|
1645
|
+
Why panel and the field drawer's "Explain for" showed "∅ Hidden", the badge of a field no set grants; "Explain for"
|
|
1646
|
+
also did not say which base Resource decides an inherited field. Row, panel and "Explain for" now draw the same
|
|
1647
|
+
badge (`pf_effective_field_badge`) and the same "Decided by …" sentence.
|
|
1648
|
+
<!-- pin: test/permission_forge/html/field_test_round_four_admin_ui_test.rb#the Why panel of a field hidden by muting reads as its row does -->
|
|
1649
|
+
<!-- pin: test/permission_forge/html/labels_and_attention_admin_test.rb#Explain for names the base Resource that decides an inherited field, as the Why panel does -->
|
|
1650
|
+
- **"Across all Permission Sets" on an action drew two glyphs** ("✎ ✓ Permitted", "⊘ ⊘ Blocked"); it now reads as
|
|
1651
|
+
the action's "Right now" badge.
|
|
1652
|
+
<!-- pin: test/permission_forge/html/state_rendering_test.rb#action permitted: Right now and Across all sets word it alike -->
|
|
1653
|
+
- **The Conditions tab dropped the "same start and end hour" error**: the form refused the window but showed no
|
|
1654
|
+
message. The error is drawn under the hour selects, which are marked invalid and point to it.
|
|
1655
|
+
<!-- pin: test/permission_forge/html/labels_and_attention_admin_test.rb#the same start and end hour is said next to the hour selects, which are marked invalid -->
|
|
1656
|
+
- **A record footer that reads "Unknown" says it once, and why.** A change written with no actor (a runner, a job)
|
|
1657
|
+
read "Last edited by Unknown" with nothing more, a record created and edited that way read "Created by Unknown · … ·
|
|
1658
|
+
Last edited by Unknown", and an explanation started with "Unknown:" again. The footer names Unknown once ("… · Last
|
|
1659
|
+
edited today"), adds that no actor was named for the change and that `PermissionForge.with_audit_actor` names one —
|
|
1660
|
+
the event page's explanation, from the same helper.
|
|
1661
|
+
<!-- pin: test/permission_forge/html/field_test_round_five_admin_ui_test.rb#the footer and the event page explain Unknown with one helper and the same words -->
|
|
1662
|
+
<!-- pin: test/permission_forge/html/field_test_round_two_copy_test.rb#a change made by code with no audit actor names it as the audit log does, with a hint -->
|
|
1663
|
+
<!-- pin: test/permission_forge/html/labels_and_attention_admin_test.rb#a record created and edited with no known actor reads Unknown once in its footer -->
|
|
1664
|
+
- **The audit event page explains an unknown actor.** An event with no actor read "Unknown · Unknown" with an "All
|
|
1665
|
+
events by this actor" link. It now names Unknown once, gives the same explanation as the record footer without
|
|
1666
|
+
repeating "Unknown:" in front of it, and the link reads "All events with no known actor".
|
|
1667
|
+
<!-- pin: test/permission_forge/html/field_test_round_three_admin_test.rb#the event page of a change with no actor explains Unknown and links to every such event -->
|
|
1668
|
+
<!-- pin: test/permission_forge/html/field_test_round_four_admin_ui_test.rb#the event page names Unknown once: the explanation does not start with it again -->
|
|
1669
|
+
- **"Created by" names who the audit log names.** A record created by the HTTP Basic admin (or an API client) has
|
|
1670
|
+
no `created_by_id` — the column holds a user id — so the footer said "Created by System" and a Permission Set's
|
|
1671
|
+
Assignments tab "by System", next to "Last edited by admin (password prompt)" and an audit log that named
|
|
1672
|
+
"admin (password prompt)". Footers, the Assignments tab, "Assigned by" (person pages, Home), the audit tabs of the
|
|
1673
|
+
Person and Permission Set pages and their CSV now take the creator from the record's creation event
|
|
1674
|
+
(`permission_set.created`, `assignment.granted`, ...) when the audit log has one, and fall back to `created_by`
|
|
1675
|
+
otherwise. The `created_by_id` / `updated_by_id` columns are unchanged.
|
|
1676
|
+
<!-- pin: test/permission_forge/html/creator_label_test.rb#a set and an assignment made by the HTTP Basic admin: footer, grid and audit tab name who the audit log names -->
|
|
1677
|
+
<!-- pin: test/permission_forge/html/creator_label_test.rb#a person stamped in created_by, with no event, is still named from the column -->
|
|
1678
|
+
- **Views decided "locked" from the record alone** (13 sites) and so offered what the server refused: Remove and bulk
|
|
1679
|
+
revoke on a locked Muting Permission Set's holders, Delete on a set in a locked group, adding or removing members of a
|
|
1680
|
+
locked group, the rule links on a locked field's drawer; the Locked badge on a row now follows the chain (a resource or
|
|
1681
|
+
rule of a locked set is locked).
|
|
1682
|
+
<!-- pin: test/permission_forge/html/locked_pages_test.rb#a locked Muting Permission Set: holders may be added and kept longer, not removed or ended sooner -->
|
|
1683
|
+
- **A locked Permission Set only offers the end dates it accepts.** The "Change end date…" menu of a locked granting set
|
|
1684
|
+
offered "30 days from today" and "No expiry", which the lock then refused; it now lists only the presets that narrow
|
|
1685
|
+
access (the other way round for a Muting Permission Set; `Admin::EditableLock.expiry_choices`). A person's Assign
|
|
1686
|
+
drawer lists a locked set that cannot take holders as disabled with a "Locked" badge and a note, instead of leaving it
|
|
1687
|
+
out. Home's "Ending soon" card shows the "Locked" badge in place of "Extend 30 days" where extending would be
|
|
1688
|
+
refused (a locked granting set), and keeps offering it for a locked Muting Permission Set, where extending narrows.
|
|
1689
|
+
<!-- pin: test/permission_forge/admin/editable_lock_test.rb#expiry_choices offers only what the lock lets through, in the set's direction -->
|
|
1690
|
+
<!-- pin: test/permission_forge/html/admin_behaviour_test.rb#the Assign drawer lists a locked granting set as locked, not hidden, and cannot assign it -->
|
|
1691
|
+
<!-- pin: test/permission_forge/html/admin_behaviour_test.rb#Home offers "Extend 30 days" only where the lock lets it through, and says Locked where not -->
|
|
1692
|
+
- **Home's privacy audit no longer offers "Fix" for a field of a locked Permission Set.** The field drawer refuses
|
|
1693
|
+
every change to a locked set's (or Resource's, or field's) fields, including a mask, so "Fix" opened a drawer with
|
|
1694
|
+
nothing to do and "This is intended" was offered when only the set was locked. Such a finding now shows the Locked
|
|
1695
|
+
badge and a "View field" link (`AttentionItems::SensitiveFinding#locked?`, by `Admin::EditableLock`), and the
|
|
1696
|
+
drawer of a locked field says "Locked, so nothing here can be saved." instead of "Saving applies to … immediately."
|
|
1697
|
+
The note under the card's title says to choose "This is intended" only while a listed finding has the button
|
|
1698
|
+
(`AttentionItems::SensitiveFinding#acknowledgeable?`).
|
|
1699
|
+
<!-- pin: test/permission_forge/html/field_test_round_three_admin_test.rb#the privacy audit shows a field of a locked set as Locked, with no Fix and no This is intended -->
|
|
1700
|
+
<!-- pin: test/permission_forge/html/field_test_round_three_admin_test.rb#the drawer of a locked field does not say what saving would do -->
|
|
1701
|
+
<!-- pin: test/permission_forge/html/field_test_round_four_admin_ui_test.rb#a privacy audit whose rows are all locked does not tell the admin to choose This is intended -->
|
|
1702
|
+
- **A locked rule that matches nothing says how it gets fixed.** On Home its row has the Locked badge (as on the
|
|
1703
|
+
privacy card) and says the set is managed by the system or API that created it, so the rule is fixed there (or the
|
|
1704
|
+
set unlocked in code): its page only offers a copy, which would leave the rule as it is.
|
|
1705
|
+
<!-- pin: test/permission_forge/html/field_test_round_five_admin_ui_test.rb#Home shows the Locked badge on a locked rule and says where it gets fixed -->
|
|
1706
|
+
- **Read-only inputs look read-only.** A locked record's inputs (the field drawer's Label and Description, the
|
|
1707
|
+
conditions' IP list) were white boxes like editable ones; they are now filled and do not light up on hover (still
|
|
1708
|
+
focusable and announced read-only), and a locked set's name and description no longer show an edit outline.
|
|
1709
|
+
<!-- pin: test/permission_forge/html/field_test_round_five_admin_ui_test.rb#read-only inputs are drawn filled, without the hover of an editable one -->
|
|
1710
|
+
<!-- pin: test/permission_forge/html/field_test_round_five_admin_ui_test.rb#a locked field drawer keeps Label and Description read-only, labelled -->
|
|
1711
|
+
- **Re-saving a filter rule that carried a relationship path turned it into a join rule.** The builder loaded the
|
|
1712
|
+
stored path of a filter rule (`join_type` nil) and saved it back as `joins`, which changed what the rule matched. It
|
|
1713
|
+
loads a path for join rules only (`PermissionSetRule#join_rule?`, the one definition), so a filter rule stays one.
|
|
1714
|
+
<!-- pin: test/support/decision_parity/rules.rb#a filter rule that carries a path stays a filter rule through the builder -->
|
|
1715
|
+
- **Fixing a misspelled relationship keeps the rule's field.** A join rule whose stored path names no relationship
|
|
1716
|
+
(`ownr`) showed the first relationship as if it were chosen next to "This model or relationship no longer exists";
|
|
1717
|
+
its select now shows "Pick a relationship…" (marked invalid) and the field select keeps the stored field
|
|
1718
|
+
(`email_address (as stored)`), so picking the relationship again keeps it when it is a column there — Save answered
|
|
1719
|
+
"Pick the field to check." and the field was lost.
|
|
1720
|
+
<!-- pin: test/permission_forge/html/field_test_round_five_admin_ui_test.rb#a relationship that is not there shows "Pick a relationship…", not the first one as if chosen -->
|
|
1721
|
+
<!-- pin: test/permission_forge/html/field_test_round_five_admin_ui_test.rb#picking the relationship again keeps the stored field when it is a column there -->
|
|
1722
|
+
- **A save the rule builder refuses names the field as the builder does.** The model's errors were shown as Rails'
|
|
1723
|
+
full messages ("Field accessor path names ownr, …", in English in every locale); they now carry the builder's own
|
|
1724
|
+
label ("Relationship names ownr, …", "Field to check names `onwer_id`, which is not a column of …", in the page's
|
|
1725
|
+
language). The API's field-keyed messages are unchanged.
|
|
1726
|
+
<!-- pin: test/permission_forge/html/field_test_round_five_admin_ui_test.rb#a refused save names the field as the builder does, in every locale -->
|
|
1727
|
+
- **Rule builder: the banner of a rule that matches nothing names the right fix.** It said "Pick the field again" for
|
|
1728
|
+
an unknown condition, join type or a value the column cannot hold; it now says what to change for each kind of
|
|
1729
|
+
problem (`RuleProblem#remedy`), and no longer repeats the stored name ("`stauts` is not a column of Customer ·
|
|
1730
|
+
stauts").
|
|
1731
|
+
<!-- pin: test/permission_forge/html/rule_builder_explanations_test.rb#the banner says the stored name once -->
|
|
1732
|
+
- **Home's "unenforced rules" and "rules that match nothing" worked on two different sets of resources** (one added
|
|
1733
|
+
`read_access`); both read `Resource.in_effect` now. A `join_type` of blanks counted as a join rule in SQL and as a
|
|
1734
|
+
filter rule in Ruby; Home and the boot warning count by `filter_join_type?`. A `requires_session_flag` /
|
|
1735
|
+
`allowed_ips` of newlines counted as a condition to the SQL scopes and not to `conditional?`; the scopes strip them
|
|
1736
|
+
too.
|
|
1737
|
+
<!-- pin: test/support/decision_parity/rules.rb#join_type_rules agrees with filter_join_type? on whitespace, for Home and the boot warning -->
|
|
1738
|
+
- **One definition per decision, read where it was re-derived.** `ManagedIndex.cache_key` reads the tenant through
|
|
1739
|
+
`PermissionForge.ambient_tenant_id`; `FieldVariants` composes a shared variant through `ResourceChain.strictest`;
|
|
1740
|
+
the cloner copies rows in the hash builder's order (`PermissionHashBuilder.sort_rows`).
|
|
1741
|
+
<!-- pin: test/permission_forge/release_readiness/one_definition_guard_test.rb#one definition: the ambient tenant (PermissionForge.ambient_tenant_id / .ambient_tenant) -->
|
|
1742
|
+
- **The People page marked every permanent assignment "due for review" while `admin_stale_assignment_days` was nil**
|
|
1743
|
+
(`nil.to_i` days); it now uses the same threshold as Home and the audit (the 90-day default).
|
|
1744
|
+
<!-- pin: test/support/decision_parity/rules.rb#stale: the People page, Home, the audit and the auditor agree with the setting nil and set -->
|
|
1745
|
+
- **The Fields tab's bulk "Add" wrote rows the presets and Sync never would.** Bulk "Editable" on new columns gave
|
|
1746
|
+
`created_at` / `updated_at` edit access (the presets and Sync give Rails' timestamps read-only), and bulk "Read-only"
|
|
1747
|
+
on `email` gave it unmasked under `gdpr_defaults` (the presets start it Masked), Editable on a Resource without Edit
|
|
1748
|
+
gave `email` Masked, and any posted name (`id`, `xyz`) got a row. The three paths now build the row in one place.
|
|
1749
|
+
<!-- pin: test/support/decision_parity/fields.rb#(a) Customer, muting=false, gdpr_defaults=true, edit: presets, bulk and the generator write the same rows -->
|
|
1750
|
+
- **Under Rails 7.2+ `permanent_connection_checkout = :disallowed` a schema read raised** (saving a Permission Set with
|
|
1751
|
+
tenancy on, a rule's column, the timestamps): reads go through the connection pool. An unreadable schema now leaves
|
|
1752
|
+
generated timestamps Read-only, not Editable.
|
|
1753
|
+
<!-- pin: test/permission_forge/schema_read_connection_test.rb#a Permission Set saves with tenancy on, and the tenant readers answer, where the deprecated reader raises -->
|
|
1754
|
+
<!-- pin: test/permission_forge/schema_read_connection_test.rb#a timestamp row stays Read-only when the columns cannot be read (fail closed) -->
|
|
1755
|
+
- **A column added after `MaskedAttributes` arrived was read raw through its getter** (`[]` and `as_json` hid it); it
|
|
1756
|
+
is guarded once Rails defines the attribute methods again.
|
|
1757
|
+
<!-- pin: test/permission_forge/concerns/masked_late_columns_test.rb#a column added after the concern arrived is guarded by its getter, as by [] and as_json -->
|
|
1758
|
+
- **"This is intended" did not reach `rake permission_forge:audit_masks` (exit 1) or the boot warning**, which also
|
|
1759
|
+
said "GDPR pattern" for a `filter_parameters` match; all four surfaces share one predicate now.
|
|
1760
|
+
<!-- pin: test/support/decision_parity/fields.rb#(b) a row marked "This is intended" is reported by no surface, and by every surface without the mark -->
|
|
1761
|
+
<!-- pin: test/support/decision_parity/fields.rb#(b) the boot warning names what matched: the GDPR pattern, or the host filter_parameters -->
|
|
1762
|
+
- **The GDPR audit reported a Hidden row as "readable unmasked".** `rake permission_forge:audit_masks`, the
|
|
1763
|
+
`unmasked_personal_data` finding and `cross_set_unmasking` took every row with `mask_access: false`, so a Hidden
|
|
1764
|
+
row (read off) of `email` was listed as personal data in the clear and as overriding another set's mask. Only a row
|
|
1765
|
+
that exposes the field (read on, no mask) is reported now.
|
|
1766
|
+
<!-- pin: test/support/decision_parity/fields.rb#(g) the GDPR audit names exactly the rows that expose a field in the clear; a Hidden row never -->
|
|
1767
|
+
- **`config.ignored_fields` given as Symbols were ignored by the field generator** (the admin honoured them): the
|
|
1768
|
+
generator created rows for `uuid`, `id`, …
|
|
1769
|
+
<!-- pin: test/support/decision_parity/fields.rb#(a) Symbol ignored_fields keep their columns out on every path -->
|
|
1770
|
+
- **A blank virtual field name made "Add resource", Sync and the Fields tab fail** (`ActiveRecord::RecordInvalid`, a
|
|
1771
|
+
500) for a host whose `extra_fields_method` lists one; the generator already skipped it. Every path skips it.
|
|
1772
|
+
<!-- pin: test/permission_forge/services/blank_name_rows_test.rb#the admin paths skip a blank virtual field name like the generator does -->
|
|
1773
|
+
- **Copying a Permission Set that holds a field or action row with a blank name failed whole** (such a row, written
|
|
1774
|
+
before the names were checked, cannot be created again). The copy leaves those rows out, copies the rest and logs
|
|
1775
|
+
how many it skipped.
|
|
1776
|
+
<!-- pin: test/permission_forge/services/blank_name_rows_test.rb#cloning a set with blank-named field and action rows skips those rows and copies the rest -->
|
|
1777
|
+
- **`GdprFieldAuditor`'s `view_all_on_pii_resource` finding listed switched-off field rows** among the resource's
|
|
1778
|
+
personal-data fields.
|
|
1779
|
+
<!-- pin: test/permission_forge/services/gdpr_field_auditor_view_all_test.rb#a switched-off field row is not among the personal-data fields of a bypassing resource -->
|
|
1780
|
+
- **A field row created without a label is labelled with its name.** A `ResourceField` created through the
|
|
1781
|
+
API or a console without a label took the install migration's column default, "Untitled Field", and the admin
|
|
1782
|
+
(Home's privacy audit, the Fields tab) showed that instead of the field; generated rows already had a label from
|
|
1783
|
+
their name. A new row whose label is blank or "Untitled Field" now gets its name as its label (`billing_email`) on
|
|
1784
|
+
create; a label set later is kept, and existing rows are not changed (rename them in the admin). Resource actions
|
|
1785
|
+
have no label default (a blank one reads as the action key).
|
|
1786
|
+
<!-- pin: test/permission_forge/models/resource_field_test.rb#a new row without a label is labelled with its name -->
|
|
1787
|
+
<!-- pin: test/permission_forge/models/resource_field_test.rb#the label default applies on create only -->
|
|
1788
|
+
- **The console shows the gem's own configuration names.** With `filter_parameters_integration` the GDPR pattern
|
|
1789
|
+
`\Aname\z` (meant for people's names) reached the gem's models through `ActiveRecord::Base.filter_attributes`, so
|
|
1790
|
+
`#inspect` printed `name: [FILTERED]` for a Permission Set, a Permission Set Group, a Resource, a field, an action
|
|
1791
|
+
and a rule. Each of these models now sets its own `filter_attributes`: the application's filters, unable to match
|
|
1792
|
+
`name`, `label` or `field_name` (configuration), still hiding whatever else they matched (a rule's `field_value`, a
|
|
1793
|
+
description). `AuditEvent` hides `actor_label` and `record_label`, which name people, whenever the application's
|
|
1794
|
+
filters hide a `name`. Request-parameter logging, SQL bind logging and the host's models are unchanged.
|
|
1795
|
+
<!-- pin: test/permission_forge/inspection_filters_test.rb#a gem model's inspect shows its name under the GDPR patterns; another attribute stays filtered -->
|
|
1796
|
+
<!-- pin: test/permission_forge/inspection_filters_test.rb#AuditEvent hides the labels that name people when the application's filters hide a name -->
|
|
1797
|
+
- **Auto-applied masking reaches the development server.** With Rails 8's development `:memory_store` (or 7.1/7.2's
|
|
1798
|
+
`:null_store`) and `auto_apply_masked_attributes = :auto`, a `Resource` row created from a runner, a console or a
|
|
1799
|
+
rake task never reached the running server, which served the model's columns in clear while `masked_field?` said
|
|
1800
|
+
`true`, until a restart. In development, with a store that cannot carry the masking token, each request or job now
|
|
1801
|
+
re-reads the `Resource` names (one query at most every 2 seconds) and masks the model at the next request. No other
|
|
1802
|
+
environment runs that query; there the boot warns that a model first named in another process stays unmasked until a
|
|
1803
|
+
restart, with the remedies (a shared store, `include PermissionForge::Concerns::MaskedAttributes` with
|
|
1804
|
+
`auto_apply_masked_attributes = false`, or a list of models). See [Cache
|
|
1805
|
+
Consistency](docs/configuration.md#cache-consistency).
|
|
1806
|
+
<!-- pin: test/permission_forge/masking_sync_test.rb#development with a per-process store masks a model named from another process at the next request -->
|
|
1807
|
+
<!-- pin: test/permission_forge/masking_sync_test.rb#development re-reads the Resource names at most every DATABASE_RECHECK seconds -->
|
|
1808
|
+
<!-- pin: test/permission_forge/cache_warnings_test.rb#production with a per-process or a null store warns that masking is not announced -->
|
|
1809
|
+
- **The masking-token warning says why, and a store that keeps nothing is not asked.** A store that keeps nothing
|
|
1810
|
+
(`cache_backend = :null`, a `:null_store`, Rails 7.1/7.2's development default) logged "The cache store did not
|
|
1811
|
+
give or keep the masking token" at boot with no cause or remedy; such a store is no longer asked for the token (the
|
|
1812
|
+
boot warning above explains what it means). For a store that fails, the warning now names what the store did — its
|
|
1813
|
+
error, or "(it did not answer, or a read right after a write came back empty: the store is down, full, or evicts the
|
|
1814
|
+
key at once)" — before the consequence and the remedy (a shared store that keeps entries, or `MaskedAttributes`
|
|
1815
|
+
included in the models).
|
|
1816
|
+
<!-- pin: test/permission_forge/masking_sync_test.rb#a store that keeps nothing is asked for no token and logs no token warning -->
|
|
1817
|
+
<!-- pin: test/permission_forge/release_readiness/masking_sync_regression_test.rb#PERF-N1b a store that does not answer is asked once per pause and logged once -->
|
|
1818
|
+
- **A gem that loads `rails/generators` at boot no longer hides the audit-table check.** The boot took any process
|
|
1819
|
+
with `Rails::Generators` loaded for a generator run and skipped the missing-table warning. It now looks for the
|
|
1820
|
+
command `bin/rails generate` / `destroy` loads (`Rails::Command::GenerateCommand` / `DestroyCommand`).
|
|
1821
|
+
<!-- pin: test/permission_forge/audit/boot_warnings_test.rb#Rails::Generators alone is no generator run -->
|
|
1822
|
+
<!-- pin: test/permission_forge/audit/boot_warnings_test.rb#a generator is running while bin/rails generate or destroy boots the app -->
|
|
1823
|
+
- **The admin's Erase and Home Clean-up said the web processes kept the cached permissions — while running in the web
|
|
1824
|
+
process.** Both recorded `process_local: true` and the log read "in its own process only: the web and job processes
|
|
1825
|
+
kept theirs" although the serving process had cleared its store (the person lost access at once). The outcome now
|
|
1826
|
+
records where it ran (`in_request`), and the sentence and the flash name the *other* web workers and the job
|
|
1827
|
+
processes.
|
|
1828
|
+
<!-- pin: test/permission_forge/audit/cache_scope_events_test.rb#cleanup.expired_assignments from an admin request says process-local for the other processes only -->
|
|
1829
|
+
- **`ConsentWithdrawalHandler` from a runner on a per-process store answered `invalidated: true` and recorded no
|
|
1830
|
+
scope.** `consent.withdrawn` carries the outcome like the other three events, the result carries `cache`, and the
|
|
1831
|
+
admin's Withdraw consent flash says when the store is per-process.
|
|
1832
|
+
<!-- pin: test/permission_forge/audit/cache_scope_events_test.rb#consent.withdrawn on the suite's memory store says process-local in the event and the result, in every locale -->
|
|
1833
|
+
- **`ExpiredAssignmentCleaner` and `BulkPermissionSetAssignment` from the admin and the API ignored the cache store's
|
|
1834
|
+
answer in the flash and said nothing about scope.** The Home Clean-up flash and the bulk assign/revoke flashes say
|
|
1835
|
+
when the store did not confirm (`maintenance.cleaned_cache_kept`, `bulk_assignments.assigned_cache_kept` /
|
|
1836
|
+
`revoked_cache_kept`) and when it is per-process.
|
|
1837
|
+
<!-- pin: test/permission_forge/html/refresh_cache_failure_test.rb#a refresh the store confirmed is announced as done -->
|
|
1838
|
+
- **`permissions.invalidated` says which processes it reached.** The runtime event (the `cache` group) now carries
|
|
1839
|
+
`process_local` and `in_request` (the scope every summary event's `Cache::Outcome` carries), so with a per-process
|
|
1840
|
+
store its audit sentence ends with the same cache-scope suffix instead of reading as if every process dropped the
|
|
1841
|
+
user's permissions; with `confirmed: false` it says the store did not confirm the removal, never that it was
|
|
1842
|
+
cleared.
|
|
1843
|
+
<!-- pin: test/permission_forge/audit/runtime_plumbing_test.rb#a rebuilt and an invalidated permission hash are published, stored only with the cache group -->
|
|
1844
|
+
- **Users made by Rails 8's `bin/rails generate authentication` show and are found by their email.** That generator
|
|
1845
|
+
names the column `email_address`, and PermissionForge only read `email`: a person without a name column showed as
|
|
1846
|
+
"User #2" in the admin, searching people by email found nobody, "Paste emails or names" in bulk assignment reported
|
|
1847
|
+
existing people as not found, and the cached profile (`permissions_hash[:profile][:email]`) was empty. The admin
|
|
1848
|
+
labels, people search and bulk matching now read `email`, then `email_address` (bulk matching tries every such
|
|
1849
|
+
column the table has), and the default `profile_builder` and the one the install generator writes fill the
|
|
1850
|
+
profile's `email` key from `email_address` when `email` is missing or blank. An existing generated initializer keeps
|
|
1851
|
+
its `profile_builder`: change its `email:` to `user.try(:email).presence || user.try(:email_address)` to get the
|
|
1852
|
+
same. Users with an `email` column see no change.
|
|
1853
|
+
<!-- pin: test/permission_forge/admin/email_address_people_test.rb#the label shows the email_address when there is no name -->
|
|
1854
|
+
<!-- pin: test/permission_forge/admin/email_address_people_test.rb#people search finds an email_address in SQL -->
|
|
1855
|
+
<!-- pin: test/permission_forge/admin/email_address_people_test.rb#the default profile_builder fills email from email_address and still prefers email -->
|
|
1856
|
+
- **An `expires_at` that is not ISO 8601 is refused.** The API (`POST …/users`, `POST …/users/bulk`) and the admin's
|
|
1857
|
+
Undo after a removal read `expires_at` with `Time.zone.parse`, which takes any text with a recognisable fragment:
|
|
1858
|
+
`"tuesday"` or `"next tuesday-ish"` became today 00:00 and was answered 201 with an assignment that had already ended,
|
|
1859
|
+
and `"2030-02-30"` became 2 March. `expires_at` is now read only as an ISO 8601 date (`2026-10-22`, now the end of
|
|
1860
|
+
that day in `Time.zone`: see **Changed**) or date and time (`2026-10-22T10:00`, optional seconds and fraction, `T` or one
|
|
1861
|
+
space, optional `Z` or `±HH:MM`); anything else is a 422 in the API (a 400 in the admin) and assigns nobody. The
|
|
1862
|
+
formats are listed in [`docs/admin-api.md`](docs/admin-api.md#expires_at-formats). The values the admin itself sends
|
|
1863
|
+
are unchanged.
|
|
1864
|
+
<!-- pin: test/permission_forge/api/permission_set_users_controller_test.rb#an expires_at that is not ISO 8601 is a 422 and assigns nobody, single and bulk -->
|
|
1865
|
+
<!-- pin: test/permission_forge/api/permission_set_users_controller_test.rb#every accepted expires_at form is stored as the time it names, single and bulk -->
|
|
1866
|
+
<!-- pin: test/permission_forge/security/bad_input_test.rb#restore with an unreadable end date is a 400, not a permanent assignment -->
|
|
1867
|
+
- **An API `permission_set_group_id` that starts with digits no longer names another group.** The integer column cast
|
|
1868
|
+
`"12ab…"` (a uuid that starts with digits) to 12, so `PATCH …/permission_sets/:uuid` could move the set into the
|
|
1869
|
+
group whose id is 12. Only a string of digits is read as an id now; anything else is a uuid (under `:uuid`) or a 422.
|
|
1870
|
+
<!-- pin: test/permission_forge/api/record_identifier_test.rb#a uuid that starts with digits is never read as a group id -->
|
|
1871
|
+
- **The 401 of a refused `api_auth` was always in the default locale** (the request's locale was assigned by a later
|
|
1872
|
+
callback); it now follows `locale` / `Accept-Language` like every other API error.
|
|
1873
|
+
<!-- pin: test/permission_forge/api/envelope_schema_test.rb#GET /api/permission_sets (permission_sets#index) answers the schema in its success and in every error class -->
|
|
1874
|
+
- **`GET …/permission_sets/new`, `…/:identifier/edit`** (and the same under resources, fields, actions, rules and
|
|
1875
|
+
groups) raised `AbstractController::ActionNotFound` — Rails' own 404 page, outside the API envelope: the API routes
|
|
1876
|
+
no longer draw `new` / `edit`, so `…/new` reads as an identifier (the JSON 404 "Record not found") and `…/edit`
|
|
1877
|
+
reaches the catch-all (the JSON 404 "No API endpoint answers this path and method").
|
|
1878
|
+
<!-- pin: test/permission_forge/api/envelope_schema_test.rb#…/new and …/:identifier/edit answer the JSON 404 envelope -->
|
|
1879
|
+
- **The audit called `current_user` on a `uuid` column a value the column cannot hold** (it judged the id as `0`); it
|
|
1880
|
+
now judges it as a number and as a uuid, and lists it only when neither fits.
|
|
1881
|
+
<!-- pin: test/permission_forge/operators/postgres_column_types_test.rb#current_user on a uuid column is a uuid only when the user id is one -->
|
|
1882
|
+
- **RLS: the policy's "no activation condition" quoted a newline, a carriage return and a tab through `squish`**,
|
|
1883
|
+
which turned them into spaces; the `view_all` EXISTS is built without it.
|
|
1884
|
+
<!-- pin: test/rls/rls_rule_parity_test.rb#a set whose condition texts are blanks is unconditional to the policy, as to the app layer -->
|
|
1885
|
+
- **`rake permission_forge:stale_assignments` refuses a `THRESHOLD_DAYS` it cannot read.** The value was read with
|
|
1886
|
+
`to_i`, so `THRESHOLD_DAYS=`, `abc`, `0` or `-3` became a threshold of 0 (or less) days and reported every permanent
|
|
1887
|
+
assignment, exiting 1. Anything that is not a whole number of days from 1 to 36500 now prints what is expected on
|
|
1888
|
+
stderr and exits 2, without running the report.
|
|
1889
|
+
<!-- pin: test/permission_forge/tasks/audit_rake_tasks_test.rb#stale_assignments refuses a THRESHOLD_DAYS that is not a number of days -->
|
|
1890
|
+
- **`rake permission_forge:audit` says why `FAIL_ON` fails the build**: `Failing: N finding(s) at or above <level>
|
|
1891
|
+
(FAIL_ON=<value>).` on stderr before the exit 1 (FORMAT=json output on stdout stays parseable).
|
|
1892
|
+
<!-- pin: test/permission_forge/tasks/rake_output_test.rb#FAIL_ON says why it fails the build -->
|
|
1893
|
+
<!-- pin: test/permission_forge/tasks/rake_output_test.rb#FORMAT=json keeps stdout parseable when FAIL_ON fails -->
|
|
1894
|
+
- **The `no_rules` finding's advice follows `auto_query_scoping`.** It said "enable auto_query_scoping" also when it
|
|
1895
|
+
was on (under `config.preset = :strict`).
|
|
1896
|
+
<!-- pin: test/permission_forge/services/security_audit_test.rb#no_rules advises enabling auto_query_scoping only while it is off -->
|
|
1897
|
+
- **Lists in the admin read in Albanian and Macedonian.** A list of people, sets or resources ("Bob Sales and Carol
|
|
1898
|
+
Support" on the set page, the review, the assign drawer, the confirmations, the "Assigned" notice) was joined by
|
|
1899
|
+
`Array#to_sentence`, which reads Rails' `support.array` keys: an app without `rails-i18n` has them in English only.
|
|
1900
|
+
The admin now joins every list with the connectors of the gem's own keys (`permission_forge.support.array.*`, in
|
|
1901
|
+
en, sq and mk; helper `pf_to_sentence`, `Admin::ListSentence`); a test fails on a bare `to_sentence` in `app/` or
|
|
1902
|
+
`lib/`.
|
|
1903
|
+
<!-- pin: test/permission_forge/html/field_test_round_five_admin_ui_test.rb#a list reads with the connectors of each locale -->
|
|
1904
|
+
<!-- pin: test/permission_forge/html/field_test_round_five_admin_ui_test.rb#nothing in app/ or lib/ joins a list with a bare to_sentence -->
|
|
1905
|
+
- **In Albanian and Macedonian the admin showed English where it had no key of its own**: Rails-core validation
|
|
1906
|
+
messages ("can't be blank", "has already been taken", or "Translation missing: sq.errors.messages.blank"),
|
|
1907
|
+
humanized column, model, association and action names in the effective-access tab, the Home privacy card, the Field
|
|
1908
|
+
lookup, the rule builder's selects and sentences, the sync panel and the "New Resource" list, the audit log's record
|
|
1909
|
+
names ("Permission Set #1" from `titleize`), the person and Permission Set audit CSV headers, the Settings page's
|
|
1910
|
+
configuration values. Each now comes from a `permission_forge.*` key or is the stored name verbatim.
|
|
1911
|
+
<!-- pin: test/permission_forge/html/locale_leak_test.rb#every screen shows only translated words and host data -->
|
|
1912
|
+
- **Locale file fixes.** The sq locale file mixed „…“ and “…” (three keys); `ui.badges.fail_open` was "Fail-open" in sq
|
|
1913
|
+
and mk; the Settings intro said "deploy" in both.
|
|
1914
|
+
<!-- pin: test/permission_forge/i18n_test.rb#sq quotes with one pair of quotation marks -->
|
|
1915
|
+
- **`pf_operator_label`, the audit log's actor type / category / source / filter labels and the condition flag
|
|
1916
|
+
labels showed an unknown value humanized** ("made up operator"); they show it as stored.
|
|
1917
|
+
<!-- pin: test/permission_forge/helpers/application_helper_test.rb#pf_operator_label shows an unknown operator as stored, never humanized -->
|
|
1918
|
+
- **Admin wording.** The empty Rules tab says "every Customer record they can read" (it said "every Customers"); the
|
|
1919
|
+
banner of a Muting Permission Set's resource says "take access away from everyone who has “No PII exports”" (it
|
|
1920
|
+
said "No PII exports's people"). A person shown by the email or the "User #2" fallback is named in full in sentences
|
|
1921
|
+
("What User #2 can do right now"; it said "What User can do"), and an email label takes its initials from the local
|
|
1922
|
+
part (`dave@acme.test` is "DA"; it was "DT"). In a Muting Permission Set the drawer of a field without a setting
|
|
1923
|
+
says the set takes nothing away from it, instead of "Blocked by default" and a link to
|
|
1924
|
+
`unconfigured_field_strategy`, which only decides for granting sets.
|
|
1925
|
+
<!-- pin: test/permission_forge/html/admin_copy_test.rb#the empty Rules tab names the resource in the singular, in every locale -->
|
|
1926
|
+
<!-- pin: test/permission_forge/html/admin_copy_test.rb#the muting banner quotes the set name instead of a possessive -->
|
|
1927
|
+
<!-- pin: test/permission_forge/html/admin_copy_test.rb#a person labelled by the fallback is named in full on the person page -->
|
|
1928
|
+
<!-- pin: test/permission_forge/admin/services_test.rb#an email label takes its initials from the local part -->
|
|
1929
|
+
<!-- pin: test/permission_forge/html/admin_copy_test.rb#the field drawer of a Muting Permission Set explains the unconfigured field the same way -->
|
|
1930
|
+
- **Adding a resource to a Muting Permission Set says what it takes away.** With "Leave everything unchanged" the toast
|
|
1931
|
+
read "Added Customer with 0 fields" and the page warned "⚠ Sync fields with model · 8 new columns not configured",
|
|
1932
|
+
although a column with no setting in a Muting Permission Set takes nothing away. The toast now says "Added Customer.
|
|
1933
|
+
It mutes nothing yet: choose below what it takes away." (or what the preset took away: "It takes away Create, Edit,
|
|
1934
|
+
and Delete."), and the sync panel is shown as information ("8 columns with no setting, so they mute nothing"). It
|
|
1935
|
+
still warns when a configured field's column is gone, and for granting sets as before
|
|
1936
|
+
(`Admin::FieldRows#sync_warning?`).
|
|
1937
|
+
<!-- pin: test/permission_forge/html/field_test_round_two_copy_test.rb#a muting resource added with nothing changed says it mutes nothing yet, and the sync panel is no warning -->
|
|
1938
|
+
<!-- pin: test/permission_forge/html/field_test_round_two_copy_test.rb#a muting resource added with a preset that takes something away says what it takes away -->
|
|
1939
|
+
- **The savebar of a Permission Set only shows up on its other tabs once there is something to save.** The
|
|
1940
|
+
Assignments tab said "Assigning or revoking applies immediately" above "No unsaved changes · Review and save". On
|
|
1941
|
+
every tab but Resources the bar is hidden while nothing joined to it (the set's name, description, group, Active)
|
|
1942
|
+
has changed; without JavaScript it stays, as the way to save those.
|
|
1943
|
+
<!-- pin: test/permission_forge/html/field_test_round_two_copy_test.rb#the set savebar is quiet on the tabs that apply at once, and stays rendered for the header -->
|
|
1944
|
+
<!-- pin: test/permission_forge/html/field_test_round_two_copy_test.rb#only a page running the script hides a quiet savebar, and only while nothing changed -->
|
|
1945
|
+
- **"Review and save" says what it did not apply.** A request that turns on Edit (or Delete, View All, Modify All)
|
|
1946
|
+
while Read is off — a form the admin never sends — was answered "No changes to save." It now says "Customer: Edit not
|
|
1947
|
+
turned on, because Read is off" (Modify All: "because it needs View All and Edit"), in the alert when nothing else
|
|
1948
|
+
changed and on the review page next to the changes that do apply (`Admin::ChangeSet#refused`).
|
|
1949
|
+
<!-- pin: test/permission_forge/html/admin_behaviour_test.rb#a crafted Edit-without-Read says Edit was not turned on because Read is off -->
|
|
1950
|
+
<!-- pin: test/permission_forge/html/admin_behaviour_test.rb#the review lists a refused flag next to the changes that do apply -->
|
|
1951
|
+
<!-- pin: test/permission_forge/admin/change_set_test.rb#Modify All without View All and Edit is refused as needing them -->
|
|
1952
|
+
- **A success toast no longer covers the "unsaved changes" bar.** On a page with that sticky bar the toasts are shown
|
|
1953
|
+
at the top of the window instead of the bottom.
|
|
1954
|
+
<!-- pin: test/permission_forge/html/admin_behaviour_test.rb#toasts move to the top of a page that has a savebar -->
|
|
1955
|
+
- **Effective access shows the whole rule in the Records cell**, wrapped, instead of cutting it mid-word ("Owner is
|
|
1956
|
+
the cur").
|
|
1957
|
+
<!-- pin: test/permission_forge/html/admin_behaviour_test.rb#the Records cell shows the whole rule, wrapped, not cut with an ellipsis -->
|
|
1958
|
+
- **Every Permission Set page scrolled the whole document sideways on a phone** (to 619px at 360–600px) and the
|
|
1959
|
+
Permission Set index to 410px, with no cue: a screen-reader-only header cell of the wide grid is `position:
|
|
1960
|
+
absolute` and the scroll frame was not its containing block, so the cell sat at its static position in the page,
|
|
1961
|
+
outside the frame's clipping, and widened the document. `.pf-scroll-x` and every scroll container are `position:
|
|
1962
|
+
relative` now, so what they clip scrolls with them.
|
|
1963
|
+
<!-- pin: test/permission_forge/html/layout_lint_test.rb#every scroll container in the stylesheet is a containing block, and the frame paints its cue -->
|
|
1964
|
+
- **Placeholders that were sentences clipped at phone width**: the Home search in every locale, the people pickers in
|
|
1965
|
+
Macedonian, the Permission Set description in Macedonian (53 characters). They are short now (a value or
|
|
1966
|
+
placeholder wider than its input ends in an ellipsis), with the sentence as a hint under the control where one is
|
|
1967
|
+
needed.
|
|
1968
|
+
<!-- pin: test/permission_forge/html/layout_lint_test.rb#every placeholder fits a phone-width box in every locale -->
|
|
1969
|
+
<!-- pin: test/permission_forge/html/field_test_round_five_admin_ui_test.rb#the Permission Set description placeholder fits a phone-width input in every locale -->
|
|
1970
|
+
- **On a phone, long content ran past the card's edge**: a setting's long option list or pattern, the how-to snippet
|
|
1971
|
+
panel, a person's long email, a long model name and the group's people picker were cut off; they wrap, or scroll
|
|
1972
|
+
inside their own box.
|
|
1973
|
+
- **The Assignments grid's column headers line up with their cells.** Each row was its own grid, so the
|
|
1974
|
+
content-sized actions column took a different width in the header (empty) than in the rows, and "Until 14 Nov"
|
|
1975
|
+
sat under "Assigned". The Assignments and Permission Sets tables now share one grid between head and rows
|
|
1976
|
+
(CSS subgrid). When the card is narrower than the table (a phone), each Assignments row stacks instead of scrolling
|
|
1977
|
+
sideways, where "Reason" was cut off.
|
|
1978
|
+
<!-- pin: test/permission_forge/html/field_test_round_three_admin_test.rb#a table with a content-sized column shares one grid between its head and rows (subgrid) -->
|
|
1979
|
+
<!-- pin: test/permission_forge/html/field_test_round_four_admin_ui_test.rb#a narrow card stacks each Assignments row instead of scrolling it -->
|
|
1980
|
+
- **The Fields grid's level controls line up across rows.** A masked row's strategy preview ("Email · j***@acme.com")
|
|
1981
|
+
was a cell of its own that pushed its level control left, and at 600px wrapped "Details ›" onto its own line. The
|
|
1982
|
+
preview now sits under the level control, no wider than it.
|
|
1983
|
+
<!-- pin: test/permission_forge/html/field_test_round_three_admin_test.rb#the mask preview sits under the level control, inside its column -->
|
|
1984
|
+
<!-- pin: test/permission_forge/html/field_test_round_three_admin_test.rb#the preview does not widen the level column -->
|
|
1985
|
+
|
|
1986
|
+
- **JSON `null`, or a blank form value, for a NOT NULL column was a 500.** `"is_active": null`, `"label": null`,
|
|
1987
|
+
`"mask_strategy": null`, a rule's `"name": null` or `"include_nulls": null` on any API create or update — and a
|
|
1988
|
+
blank Active flag on the admin's group form (a boolean column casts `""` to nil) — raised
|
|
1989
|
+
`ActiveRecord::NotNullViolation`. The eight permission models now refuse nil for every column the schema declares
|
|
1990
|
+
NOT NULL (`ValidatesRequiredColumns`, read from the schema, so a new column is covered; the primary key, the
|
|
1991
|
+
timestamps and `uuid` are filled on create, a column with a presence rule of its own or of a required `belongs_to`
|
|
1992
|
+
keeps that message): a 422 keyed by the attribute in the API, the form again with the message in the admin (a blank
|
|
1993
|
+
flag is no boolean: "must be true or false"). A NOT NULL violation no validation foresaw is the 422 envelope (`base`)
|
|
1994
|
+
in the API and a flash in the admin.
|
|
1995
|
+
<!-- pin: test/permission_forge/security/not_null_input_test.rb#API: JSON null or a blank form value on a NOT NULL column is a 422 keyed by the attribute, on create and update -->
|
|
1996
|
+
<!-- pin: test/permission_forge/security/not_null_input_test.rb#admin: a blank Active flag on the group form goes back to the group with the message -->
|
|
1997
|
+
<!-- pin: test/permission_forge/security/not_null_input_test.rb#API: an unforeseen NotNullViolation is a 422 envelope -->
|
|
1998
|
+
<!-- pin: test/permission_forge/concerns/validates_required_columns_test.rb#every permission model refuses nil in each of its NOT NULL columns -->
|
|
1999
|
+
- **A Hash or a list in the audit tab's `days`, `who`, `type` or `views` was a 500** (a Permission Set's audit tab, its
|
|
2000
|
+
CSV, and a person's audit tab). They are read as typed text, `days` as one of the offered counts.
|
|
2001
|
+
<!-- pin: test/permission_forge/security/bad_input_test.rb#the audit tab reads a Hash or a list in days, who, type or views as no filter -->
|
|
2002
|
+
- **A negative or huge `rule[remove_step]` in the rule builder was a 500** (`path.first(-1)`); it is read as a count
|
|
2003
|
+
of steps between 0 and the path's length, else no removal.
|
|
2004
|
+
<!-- pin: test/permission_forge/security/bad_input_test.rb#the rule builder reads a negative or huge remove_step as no removal -->
|
|
2005
|
+
- **The person list read `page=2abc` as page 2.** A page is read by `ParamShapes.integer` there too: anything that is
|
|
2006
|
+
not a whole number is the first page.
|
|
2007
|
+
<!-- pin: test/permission_forge/release_readiness/integer_param_guard_test.rb#a page that is not a whole number is the first page of people -->
|
|
2008
|
+
- **An audit sentence said "cleared" when the cache store said no.** `cache.cleared` with `cache_confirmed: false` and
|
|
2009
|
+
`permissions.invalidated` with `confirmed: false` read "cleared the cached permissions", and on a per-process store
|
|
2010
|
+
the suffix added "cleared in its own process only". They now say the store did not confirm the clear, and the suffix
|
|
2011
|
+
for an unconfirmed operation says only that the store lives in each process (`audit_log.cache_scope.unconfirmed`,
|
|
2012
|
+
en/sq/mk).
|
|
2013
|
+
<!-- pin: test/permission_forge/audit/cache_sentence_truth_test.rb#a clear of every cache the store did not confirm is not told as cleared -->
|
|
2014
|
+
<!-- pin: test/permission_forge/audit/cache_sentence_truth_test.rb#an invalidation the store did not confirm is not told as cleared -->
|
|
2015
|
+
<!-- pin: test/permission_forge/audit/cache_sentence_truth_test.rb#the scope suffix of an unconfirmed operation on a per-process store does not say cleared either -->
|
|
2016
|
+
- **An invalidation inside a host's web request said the web processes kept their cached permissions — from a web
|
|
2017
|
+
process.** `in_request` was true only for the admin and the API; it is now true for any code of a web server
|
|
2018
|
+
process (`Cache::Report.in_request?`, the one definition, with `Warnings.server_process?`), and the suffix for a
|
|
2019
|
+
process of its own no longer names the web processes ("…cleared only in the process that ran it (an in-memory
|
|
2020
|
+
store): every other process kept theirs.").
|
|
2021
|
+
<!-- pin: test/permission_forge/audit/cache_sentence_truth_test.rb#an invalidation inside a host controller request of a server process records in_request -->
|
|
2022
|
+
<!-- pin: test/permission_forge/audit/cache_sentence_truth_test.rb#the own-process suffix never names the web processes -->
|
|
2023
|
+
- **`OPTIONS` and `TRACE` under the API, and a form or multipart body Rack cannot parse, answered Rails' own pages.**
|
|
2024
|
+
The verbs reached no route (`RoutingError`); a body such as `a[=1&%%%` raised `ActionController::BadRequest` while
|
|
2025
|
+
Rails read the parameters for its instrumentation, before `rescue_from`, giving Rails' 400 in `text/html`. The verbs
|
|
2026
|
+
now get the JSON 404 envelope, the bodies the 400 envelope an unreadable JSON body gets.
|
|
2027
|
+
<!-- pin: test/permission_forge/api/unmatched_routes_test.rb#OPTIONS and TRACE under the API answer the JSON 404 envelope -->
|
|
2028
|
+
<!-- pin: test/permission_forge/api/envelope_schema_test.rb#~answers the schema in its success and in every error class -->
|
|
2029
|
+
|
|
2030
|
+
- **Every admin form works in an API-only application.** `rails new --api` also leaves out `Rack::MethodOverride`, so
|
|
2031
|
+
with the three middlewares docs/admin-ui.md names, every edit and delete form (a POST with a hidden `_method`) answered
|
|
2032
|
+
404 "No route matches [POST] …" (field test round 7, D#67). The engine brings its own,
|
|
2033
|
+
`PermissionForge::AdminMethodOverride`, in the engine's middleware stack: it runs for the admin's paths only (a
|
|
2034
|
+
full-stack host's own is a no-op before it) and skips the JSON API's. A full-stack host's own `Rack::MethodOverride`
|
|
2035
|
+
runs before the router for every path, the API's included, as Rails has it: there a POST with `_method` or
|
|
2036
|
+
`X-HTTP-Method-Override` reaches the API as that verb, and `api_auth`, the forgery posture and the authorization
|
|
2037
|
+
lambdas still decide it (docs/admin-api.md "Method override"). An API-only host has none, so its API sees the verb
|
|
2038
|
+
that was sent.
|
|
2039
|
+
<!-- pin: test/permission_forge/engine_method_override_test.rb#an admin form posted with _method=delete reaches the destroy route -->
|
|
2040
|
+
<!-- pin: test/permission_forge/engine_method_override_test.rb#the JSON API is left alone: _method and the override header do not change its verb -->
|
|
2041
|
+
<!-- pin: test/permission_forge/engine_method_override_test.rb#in a full-stack host the host's Rack::MethodOverride applies to the JSON API too, behind api_auth -->
|
|
2042
|
+
<!-- pin: test/permission_forge/docs/doc_samples_test.rb#an API-only host: the JSON API works, the admin names the middlewares it needs -->
|
|
2043
|
+
- **The admin reads a person's encrypted name or email decrypted.** With `encrypts :email` (or `:name`) on the user
|
|
2044
|
+
model, people labels showed the ciphertext JSON, the people search's LIKE matched it (`iv` found everyone) and bulk
|
|
2045
|
+
assignment compared it with the pasted text (nobody). Labels read such a column through its attribute type; the
|
|
2046
|
+
search and bulk matching leave it out, and match a deterministically encrypted one against the whole value exactly.
|
|
2047
|
+
A value this application cannot decrypt (another key) is no label, also under `support_unencrypted_data`, where
|
|
2048
|
+
Rails answers it with the stored ciphertext; a value stored in plaintext there reads as written. The default
|
|
2049
|
+
`profile_builder` leaves such an attribute out (Changed).
|
|
2050
|
+
<!-- pin: test/permission_forge/encrypted_attributes_test.rb#decrypt is nil for ciphertext under another key, whether support_unencrypted_data is on or off -->
|
|
2051
|
+
<!-- pin: test/permission_forge/encrypted_attributes_test.rb#decrypt reads a value stored in plaintext under support_unencrypted_data, and nil without it -->
|
|
2052
|
+
<!-- pin: test/permission_forge/admin/encrypted_people_test.rb#a label reads the name and email decrypted, never the ciphertext -->
|
|
2053
|
+
<!-- pin: test/permission_forge/admin/encrypted_people_test.rb#the search never matches the ciphertext -->
|
|
2054
|
+
<!-- pin: test/permission_forge/admin/encrypted_people_test.rb#bulk matching finds a deterministically encrypted email exactly, and nobody by an encrypted name -->
|
|
2055
|
+
- **A missing name or value is said once.** A Permission Set or a group created without a name answered
|
|
2056
|
+
`{"name": ["can't be blank", "can't be blank"]}` (a rule without a value too, on `field_value`): the presence rule
|
|
2057
|
+
holds a condition, and the NOT NULL check (`ValidatesRequiredColumns`) did not see it. The NOT NULL check now runs
|
|
2058
|
+
after every other validation and adds nothing to an attribute that already has an error, whatever the order the
|
|
2059
|
+
validations were declared in (a canary watches the `validate` chain's `:after` callbacks).
|
|
2060
|
+
<!-- pin: test/permission_forge/concerns/validates_required_columns_test.rb#a column that already has an error gets no second one, whatever the order of the validators -->
|
|
2061
|
+
<!-- pin: test/permission_forge/api/generated_defaults_api_test.rb#POST create without a name or a value names each once -->
|
|
2062
|
+
|
|
2063
|
+
- **The Conditions tab explains a condition with the definition that judges it.** A window from 9 to 9 read "☾
|
|
2064
|
+
Overnight: active from 09:00 until 09:00 the next morning" under its own error, and the tab's summary sent the reader
|
|
2065
|
+
"to the Conditions tab" they were on (field test round 7, E#32): the tab asks `ActivationConditions::HoursWindow`
|
|
2066
|
+
(`set?`, `wraps?`, `broken?`), and its summary has an on-tab form. An IP list with an entry that is no address read
|
|
2067
|
+
"Only works from office-network." (B#48a): the summary says the list cannot be judged, and one banner lists every
|
|
2068
|
+
problem `ActivationConditions#condition_problems` names, with the effect Home's card names (a granting set grants
|
|
2069
|
+
nothing, a muting set applies to every request; one key for both, `ui.conditions.effect`). The form refuses what
|
|
2070
|
+
the model refuses for the same reason (`HoursWindow#problem`, an hour outside 0–23 included;
|
|
2071
|
+
`ActivationConditions.ip_range`).
|
|
2072
|
+
<!-- pin: test/permission_forge/html/conditions_explanations_test.rb#a window from an hour to the same hour is never shown as overnight, nor sent to the tab it is on -->
|
|
2073
|
+
<!-- pin: test/permission_forge/html/conditions_explanations_test.rb#a granting set's list with an entry that is no address shows one banner with Home's effect -->
|
|
2074
|
+
<!-- pin: test/permission_forge/html/conditions_explanations_test.rb#the form refuses an hour outside the day for the reason the model has -->
|
|
2075
|
+
<!-- pin: test/permission_forge/release_readiness/one_definition_guard_test.rb#one definition: an hour window: set, overnight, broken (ActivationConditions::HoursWindow) -->
|
|
2076
|
+
- **A stored rule on a secret column says so where Home's "Fix rule" leads.** The builder opened with no banner and a
|
|
2077
|
+
preview that read as fine (field test round 7, B#48b). `Admin::RuleNotes.for(rule)` is the one list of what is said
|
|
2078
|
+
beside a stored rule — why it matches nothing (`RuleProblem`), then a secret column (refused for a new rule; this one
|
|
2079
|
+
still applies) — drawn by the builder's banner, the preview and a badge on the Rules tab's row. The builder no
|
|
2080
|
+
longer offers a column declared with `encrypts`, beside the secret ones, except the one a stored rule names.
|
|
2081
|
+
<!-- pin: test/permission_forge/html/rule_notes_test.rb#Home's Fix rule opens a builder that says the rule compares a secret column -->
|
|
2082
|
+
<!-- pin: test/permission_forge/html/rule_notes_test.rb#the preview of that rule says it too, and the Rules tab badges the row -->
|
|
2083
|
+
<!-- pin: test/permission_forge/html/rule_notes_test.rb#the builder offers no encrypted column, but keeps the one a stored rule names -->
|
|
2084
|
+
- **A rule's sentence reads its operators from the definitions that decide.** "The current user" is said only where
|
|
2085
|
+
the user's id is substituted (one value compared, `ValueTyping.current_user?`), not in a list, where the word is
|
|
2086
|
+
compared as written; a range's ends are split as `ValueTyping.split` splits them; "or empty" follows
|
|
2087
|
+
`Operators::Registry::NULL_INCLUDING`, the list the RLS policy reads too. `SentencesHelper` keeps no operator list of
|
|
2088
|
+
its own.
|
|
2089
|
+
<!-- pin: test/permission_forge/helpers/rule_sentence_operators_test.rb#"the current user" is said only where one value is compared -->
|
|
2090
|
+
<!-- pin: test/permission_forge/helpers/rule_sentence_operators_test.rb#the operators that add the NULL rows are the strategies that do, and the policy reads the same list -->
|
|
2091
|
+
- **A Permission Set with two Resources of one single-table-inheritance chain is named with the Resource.** The field
|
|
2092
|
+
drawer's "Across all Permission Sets" showed "Sales rep ∅ Hidden" and "Sales rep ◉ Read-only" side by side, and a
|
|
2093
|
+
person's effective access "from Sales rep" (field test round 7, B#35); such a row reads "Sales rep · VipCustomer",
|
|
2094
|
+
the model marked as host data (`Admin::ChainEntries`, `pf_set_entry_name`). A Muting Permission Set's reasons there —
|
|
2095
|
+
a flag, a field or an action it takes away — name the Resource whose row does the muting the same way
|
|
2096
|
+
(`pf_muting_set_name`).
|
|
2097
|
+
<!-- pin: test/permission_forge/html/sti_set_entries_test.rb#the drawer's rows name the Resource when the set holds more than one of the chain -->
|
|
2098
|
+
<!-- pin: test/permission_forge/html/sti_set_entries_test.rb#a person's effective access names the Resource the same way -->
|
|
2099
|
+
<!-- pin: test/permission_forge/html/sti_set_entries_test.rb#a person's effective access names the muting Resource the same way -->
|
|
2100
|
+
- **Home counts people with one Macedonian noun.** "1 лице го има овој сет" stood beside "1 корисник" (field test round
|
|
2101
|
+
7, C#92); every count of people on Home uses the noun of `ui.people_count`, in every locale (a glossary test).
|
|
2102
|
+
<!-- pin: test/permission_forge/locale_glossary_test.rb#mk: Home counts people with the noun of ui.people_count -->
|
|
2103
|
+
- **A save that changed nothing says "No changes to save." on every admin form.** The action form said "Saved
|
|
2104
|
+
“Export”. It applies immediately.", the rule builder "Saved the rule “Own customers”.", the group page "Group saved.
|
|
2105
|
+
No one's access changed.", the Conditions tab "Activation conditions saved.", an applied batch "Saved 0 changes", and
|
|
2106
|
+
an end date set to the one the assignment had restated it — none of them changed a row or wrote an audit event (field
|
|
2107
|
+
test round 8, B#70). "The save changed something" has one definition, `RecordsAuditEvents#saved_audited_changes?`
|
|
2108
|
+
(saved changes minus the bookkeeping columns), which also decides whether the change event is written, with its twin
|
|
2109
|
+
before the save, `#audited_changes_pending?`: the admin saves a record only when it holds such a change
|
|
2110
|
+
(`SaveNotice#pf_save_changed`), so an unchanged submit no longer invalidates every holder's cached permissions (Rails
|
|
2111
|
+
runs the commit callbacks of a save that wrote nothing). The flash goes through `SaveNotice#pf_save_flash`, with no
|
|
2112
|
+
cache note; an apply whose every requested change was refused says why, as the review step does. The group keeps its
|
|
2113
|
+
sentence for a change that affects nobody. The key moved from `flash.permission_sets.no_changes` to
|
|
2114
|
+
`flash.no_changes`.
|
|
2115
|
+
<!-- pin: test/permission_forge/html/unchanged_save_flash_test.rb#every admin form that saves a record, submitted unchanged, says No changes to save, writes no event, keeps the cache -->
|
|
2116
|
+
<!-- pin: test/permission_forge/html/unchanged_save_flash_test.rb#every walked form submitted with a change says what it saved, writes one event, invalidates the holder -->
|
|
2117
|
+
<!-- pin: test/permission_forge/html/unchanged_save_flash_test.rb#an apply whose every change was refused says why, as the review does -->
|
|
2118
|
+
<!-- pin: test/permission_forge/html/unchanged_save_flash_test.rb#no admin controller decides by itself whether a save changes something -->
|
|
2119
|
+
<!-- pin: test/permission_forge/html/unchanged_save_flash_test.rb#every template with a PATCH form is classified: walked as a save, or why it is not one -->
|
|
2120
|
+
<!-- pin: test/permission_forge/release_readiness/one_definition_guard_test.rb#one definition: a save changed something (RecordsAuditEvents#saved_audited_changes?, #audited_changes_pending?) -->
|
|
2121
|
+
- **The rule builder names a relationship path in the page's language.** Under the relationship picker it printed
|
|
2122
|
+
`field_accessor_path: ownr` — the column's name — in every locale, while the banner and the picker's label said
|
|
2123
|
+
"Relationship" in the page's language (field test round 8, B#95); it reads "Relationship path: `ownr`" (en, sq, mk).
|
|
2124
|
+
A walk of every admin screen, error state and posted page now refuses a column name of the gem's tables followed by a
|
|
2125
|
+
colon anywhere but the Technical details panels and the initializer snippets.
|
|
2126
|
+
<!-- pin: test/permission_forge/html/raw_attribute_names_test.rb#the rule builder names the stored relationship path in the page language -->
|
|
2127
|
+
<!-- pin: test/permission_forge/html/raw_attribute_names_test.rb#no screen prints a column name of the gem as a label -->
|
|
2128
|
+
- **A person's Effective access row names the Resource a Muting Permission Set acts through.** On a single-table-
|
|
2129
|
+
inheritance subclass, Email (muted on Customer) and Phone (muted on VipCustomer) both read "Hidden by No PII exports"
|
|
2130
|
+
beside "from Sales rep · VipCustomer", while the Why panel said "No PII exports · Customer" / "· VipCustomer" (field
|
|
2131
|
+
test round 8, B#38): the row named the set by itself, and the Why panel found the Resource by walking the set's rows
|
|
2132
|
+
again (a guess that missed a field variant and a row whose mute the resource's Read made moot). Each
|
|
2133
|
+
`EffectiveAccess::FieldSource` now carries the Resource whose row it credits (`resource_name`: the deciding entry's
|
|
2134
|
+
for a grant, the muting row's for a mute) and each blocked flag or action the Resource whose row blocks it
|
|
2135
|
+
(`Outcome#muted_on`, the row nearest the model first); the row, the Why panel and "Explain for" name every set
|
|
2136
|
+
through `pf_field_source_set_name` / `pf_muting_set_name`. The row and "Explain for" also word the level the
|
|
2137
|
+
deciding mutes give and name the same sets (`FieldOutcome#deciding_mutes`: the strongest mute, when its level is the
|
|
2138
|
+
level shown, and every set at it): "Masked by MaskA" / "Ana sees it masked: MaskA masks it.", where the row said
|
|
2139
|
+
"Hidden by" next to a Masked badge and "Explain for" said "MaskA and NoEditB hides or masks it" (counted now: "HideA
|
|
2140
|
+
and HideB hide it", per-level keys in en, sq and mk). A mute that only takes editing away decides nothing when a
|
|
2141
|
+
grant shows the field masked (the Why panel still lists it), and is not credited at all when the Resource gives no
|
|
2142
|
+
Edit: the field reads "from Sales rep", shown as Read-only because Edit isn't allowed.
|
|
2143
|
+
<!-- pin: test/permission_forge/html/sti_field_reasons_test.rb#the row names the Resource whose row mutes the field, as the Why panel and the grants beside it do -->
|
|
2144
|
+
<!-- pin: test/permission_forge/html/sti_field_reasons_test.rb#a muting reason words the level the mute decides, not always Hidden -->
|
|
2145
|
+
<!-- pin: test/permission_forge/html/sti_field_reasons_test.rb#Explain for words the deciding mute's level and names the sets the row names -->
|
|
2146
|
+
<!-- pin: test/permission_forge/html/sti_field_reasons_test.rb#two sets that hide a field are counted in Explain for -->
|
|
2147
|
+
<!-- pin: test/permission_forge/html/sti_field_reasons_test.rb#a mute that only takes editing away from a masked field does not decide it: the mask does -->
|
|
2148
|
+
<!-- pin: test/permission_forge/html/sti_field_reasons_test.rb#an edit mute on a resource that gives no Edit changes nothing and is not credited -->
|
|
2149
|
+
<!-- pin: test/permission_forge/admin/effective_access_test.rb#each source names the Resource whose row it credits; a set with two rows, the one nearest the model -->
|
|
2150
|
+
<!-- pin: test/permission_forge/release_readiness/one_definition_guard_test.rb#one definition: the Resource a set acts through on a person's effective access (FieldSource#resource_name, Outcome#muted_on) -->
|
|
2151
|
+
- **A field hidden by the entry it inherits says so on every surface.** Bob's VipCustomer › Notes row read "∅ Hidden ·
|
|
2152
|
+
No set shows it" and "Explain for" said "No set shows this field to Bob.", beside "Sales rep · VipCustomer ◉
|
|
2153
|
+
Read-only", while the Why panel said Customer decides (field test round 8, B#39). What decides is the granting
|
|
2154
|
+
Customer entry, which hides Notes — not No PII exports, whose mute on Customer.notes changes nothing the granting
|
|
2155
|
+
sets showed, so it still never reads "Hidden by muting". `EffectiveAccess::FieldOutcome#reason` is the one decision
|
|
2156
|
+
(`:muted`, `:unconfigured`, `:granted`, `:implicit`, `:unreadable`, `:inherited`, `:none`) the row, its colour, the
|
|
2157
|
+
Why panel and "Explain for" all word: an inherited hidden field reads "Hidden on Customer, which this inherits from."
|
|
2158
|
+
on all three (Explain for draws no other sentence then), and an entry without Read says why on the row too, where it
|
|
2159
|
+
read "No set shows it".
|
|
2160
|
+
<!-- pin: test/permission_forge/html/sti_field_reasons_test.rb#a field hidden on the base reads the same on the row, the Why panel and Explain for, in every locale -->
|
|
2161
|
+
<!-- pin: test/permission_forge/html/sti_field_reasons_test.rb#every field of the subclass reads the same reason on the row, the Why panel and Explain for, in every locale -->
|
|
2162
|
+
<!-- pin: test/permission_forge/html/sti_field_reasons_test.rb#the hidden-on reason follows the decision: muting the subclass row too still leaves the base deciding -->
|
|
2163
|
+
<!-- pin: test/permission_forge/html/sti_field_reasons_test.rb#a subclass entry without Read says so on the row, as the Why panel does -->
|
|
2164
|
+
<!-- pin: test/permission_forge/html/sti_field_reasons_test.rb#Explain for draws no empty sentence when the inherited entry decides -->
|
|
2165
|
+
<!-- pin: test/permission_forge/release_readiness/one_definition_guard_test.rb#one definition: why a person's field is at its level (EffectiveAccess::FieldOutcome#reason) -->
|
|
2166
|
+
- **A muting row on a single-table-inheritance base mutes the variants of the subclass entries it reaches.** This
|
|
2167
|
+
widens protection: a muting set whose base Resource hid `price_cents` left the subclass entry's `price` (a reader
|
|
2168
|
+
only the subclass defines, a configured field variant) readable and editable in the permission hash, so the derived
|
|
2169
|
+
value of a hidden column stayed readable through the subclass, while the admin showed it hidden (field test round 8,
|
|
2170
|
+
review P1). `PermissionHashBuilder` took the variants from the muting row's Resource; it now takes each reached
|
|
2171
|
+
entry's own (`FieldVariants` of the entry's model), the variants the granting phase wrote. The hash format is
|
|
2172
|
+
unchanged.
|
|
2173
|
+
<!-- pin: test/permission_forge/services/permission_hash_builder_muting_test.rb#a base row mutes the variants of each entry it reaches, a reader only the subclass has included -->
|
|
2174
|
+
<!-- pin: test/permission_forge/admin/effective_access_test.rb#a base row hides a variant only the subclass has, in the engine and here -->
|
|
2175
|
+
- **An app with no database configured boots.** A fresh `rails new --skip-kamal` app comments its production database
|
|
2176
|
+
paths out (the deploy supplies `DATABASE_URL`), and `RAILS_ENV=production SECRET_KEY_BASE_DUMMY=1 bin/rails
|
|
2177
|
+
assets:precompile` (the Dockerfile's build step) or any production runner aborted once the gem was installed: the
|
|
2178
|
+
boot ping rescued Active Record's errors only, and the sqlite3 adapter refuses a configuration without a file with an
|
|
2179
|
+
`ArgumentError` (field test round 8, E#7). `PermissionForge::DatabaseReach` is the one definition of "this process
|
|
2180
|
+
cannot use its database" (Active Record's errors and an adapter's `ArgumentError` for its configuration, rescued
|
|
2181
|
+
only around Rails' own connection code, a ping or `table_exists?`). The boot ping, auto-applied masking, the audit
|
|
2182
|
+
table checks and the audit's `audit_trail` check ask it; the audit table check of `Audit.record` keeps its narrower
|
|
2183
|
+
list (`UNREACHABLE_ERRORS`: a database that does not answer or does not exist), so a configuration error there still
|
|
2184
|
+
rolls the change back under `audit_change_failure_strategy :raise`. The boot skips its database checks with one line
|
|
2185
|
+
naming the cause (`The database could not be reached at boot, or the gem's models could not be loaded
|
|
2186
|
+
(ArgumentError: No database file specified. ...)`), and a request
|
|
2187
|
+
or job of a process that has not synced reads neither the Resource rows nor the masking token until the Resource
|
|
2188
|
+
table can be read: a production runner of such an app — Solid Cache, its production cache store, has no database
|
|
2189
|
+
either — logs no SECURITY warning ("masking could not be synced", "The cache store did not give or keep the masking
|
|
2190
|
+
token"). A missing table is not remembered either: Active Record's schema cache kept the "no" of a process that
|
|
2191
|
+
booted before the Resource table existed, which then never masked until a restart; `DatabaseReach.table_exists?`
|
|
2192
|
+
drops a "no" from the cache, so such a process syncs at its first request or job after the migration. A model whose
|
|
2193
|
+
table check raises is named in its unmaskable warning with the error. A process that runs
|
|
2194
|
+
nothing but asset tasks (`assets:precompile`, `assets:clean`, `assets:clobber`: `BootChecks.asset_build?`) runs the
|
|
2195
|
+
configuration warnings only and never connects. (An adapter whose driver gem is missing still fails as Active Record
|
|
2196
|
+
loads, as it does without the gem.)
|
|
2197
|
+
<!-- pin: test/permission_forge/install/production_boot_without_database_test.rb#assets:precompile boots in production with no database file configured, and never connects -->
|
|
2198
|
+
<!-- pin: test/permission_forge/install/production_boot_without_database_test.rb#a production runner with no database file configured boots, says the checks were skipped, and runs -->
|
|
2199
|
+
<!-- pin: test/permission_forge/engine_boot_paths_test.rb#DatabaseReach.problem counts an adapter that refuses its configuration as no database -->
|
|
2200
|
+
<!-- pin: test/permission_forge/engine_boot_paths_test.rb#MaskedModels.apply answers false, without raising, when the database is not configured -->
|
|
2201
|
+
<!-- pin: test/permission_forge/engine_boot_paths_test.rb#a process that has not synced and cannot use its database does not read the masking token -->
|
|
2202
|
+
<!-- pin: test/permission_forge/engine_boot_paths_test.rb#a model whose table check raises is named with the error in the unmaskable warning -->
|
|
2203
|
+
<!-- pin: test/permission_forge/engine_boot_paths_test.rb#a configuration error of the audit table check rolls the change back; a database gone is unchecked -->
|
|
2204
|
+
<!-- pin: test/permission_forge/masking_sync_test.rb#a process that found no Resource table syncs at its next run once the table is there -->
|
|
2205
|
+
<!-- pin: test/permission_forge/engine_boot_paths_test.rb#asset_build? is true only when every task Rake was asked for is an asset task -->
|
|
2206
|
+
- **A production boot no longer calls a `:memory_store` "the Rails development default".** The masking warning of a
|
|
2207
|
+
production boot with `config.cache_store = :memory_store` said so (field test round 8, E#66); the clause is now
|
|
2208
|
+
added in a development boot only (`CacheWarnings::DEVELOPMENT_DEFAULT`).
|
|
2209
|
+
<!-- pin: test/permission_forge/cache_warnings_test.rb#a :memory_store is called the development default in a development boot only -->
|
|
2210
|
+
- **The JSON API says which authorization lambda it lacks before the first 403.** With `api_enabled` and `api_auth`
|
|
2211
|
+
set but `api_action_authorization`, `api_assignment_authorization` or `api_stats_authorization` unset, the API refused
|
|
2212
|
+
every change, assignment or stats read (each 403 names the option) and nothing said so earlier (field test round 8,
|
|
2213
|
+
D#7; round 7 #12). The server's boot now logs one note naming each missing lambda and what the API refuses without
|
|
2214
|
+
it (`api_authorization_unset`, audience `:server`), `rake permission_forge:audit` lists them as the info finding
|
|
2215
|
+
`api_authorization_unset`, both read from `PermissionForge::ApiAuthorizations`, and the generated initializer shows
|
|
2216
|
+
`api_stats_authorization` beside the other two. The stats item says that an `api_auth` setting the deprecated
|
|
2217
|
+
`@admin = true` still passes until 0.16 (the note cannot see it).
|
|
2218
|
+
<!-- pin: test/permission_forge/security/api_require_authorization_default_test.rb#the boot names each authorization lambda the API needs and does not have, and what it refuses -->
|
|
2219
|
+
<!-- pin: test/permission_forge/services/security_audit_test.rb#an API with api_auth whose authorization lambdas are unset gets an info note naming each and what it guards -->
|
|
2220
|
+
<!-- pin: test/permission_forge/security/api_require_authorization_default_test.rb#the install initializer shows all three authorization lambdas -->
|
|
2221
|
+
- **A blank label is "no label" on every write path: `null`, `""` and white space store the row's name.** Through the
|
|
2222
|
+
JSON API a field row's `label: null` was a 422 `{"label": ["can't be blank"]}` while `label: ""` answered 200 and
|
|
2223
|
+
stored the empty label, and an action row stored either as sent; the admin stored a blanked field label as the name
|
|
2224
|
+
and a blanked action label as `""` (field test round 8, D#25). One model concern, `Concerns::NormalizesLabel`, now
|
|
2225
|
+
stores a blank label — and on a new row the install's `'Untitled Field'` column default — as the row's name, on create
|
|
2226
|
+
and on update, in code, through the API (200 / 201) and in the admin alike; a Resource's blank label is stored as
|
|
2227
|
+
`nil`. A rename relabels a row whose label the gem would generate (the old name or its humanized form:
|
|
2228
|
+
`GeneratedText.label?`) instead of leaving
|
|
2229
|
+
the old name to read as a label an admin wrote. Checked on create, when the label changes and on a rename only: a
|
|
2230
|
+
row stored with a blank label keeps it through a save of its other attributes, whose audit event names no label
|
|
2231
|
+
change. **No opt-out:** a label was never meant to be empty; write a label of
|
|
2232
|
+
your own to show something else.
|
|
2233
|
+
<!-- pin: test/permission_forge/models/normalizes_label_test.rb#a blank field or action label is stored as the name, on create and on update -->
|
|
2234
|
+
<!-- pin: test/permission_forge/models/normalizes_label_test.rb#a blank Resource label is stored as nil -->
|
|
2235
|
+
<!-- pin: test/permission_forge/models/normalizes_label_test.rb#a save of other attributes leaves a stored blank label, and its event says nothing of a label -->
|
|
2236
|
+
<!-- pin: test/permission_forge/models/normalizes_label_test.rb#a rename relabels a row whose label was generated, and keeps one someone wrote -->
|
|
2237
|
+
<!-- pin: test/permission_forge/api/blank_label_api_test.rb#a blank label is stored as the name on PATCH, for a field row and an action row alike -->
|
|
2238
|
+
<!-- pin: test/permission_forge/api/blank_label_api_test.rb#a blank label is stored as the name on POST, for a field row and an action row alike -->
|
|
2239
|
+
<!-- pin: test/permission_forge/html/editing_flows_test.rb#an action label an admin blanks is stored as the name, on create and on edit -->
|
|
2240
|
+
- **Rails 8.2: an alias of an attribute the query did not load raises `ActiveModel::MissingAttributeError`, as plain
|
|
2241
|
+
Rails does.** Rails 8.2 reads an alias with `_read_attribute!`, and its `_read_attribute` no longer takes the block
|
|
2242
|
+
that raised for an attribute not loaded (`select(:id)`), so the alias reader `MaskedAttributes` defines returned `nil`
|
|
2243
|
+
there. It uses `_read_attribute!` where Rails has it, and the block form on 7.1–8.1 (the Rails-internals canary checks
|
|
2244
|
+
both).
|
|
2245
|
+
<!-- pin: test/permission_forge/concerns/masked_attribute_aliases_test.rb#that parent and its other subclasses answer as if the concern were nowhere -->
|
|
2246
|
+
### Documentation
|
|
2247
|
+
|
|
2248
|
+
- [Migration guide](docs/migration-guide.md#upgrading-to-015): "Upgrading to 0.15" — run `rake
|
|
2249
|
+
permission_forge:audit` first, the behaviour changes with their opt-outs, the locale keys that moved, the removed
|
|
2250
|
+
deprecations, and the RLS re-sync (`GENERATOR_VERSION` 7).
|
|
2251
|
+
<!-- pin: docs/migration-guide.md#upgrading-to-015 -->
|
|
2252
|
+
- Every `run` and `file=` block of README.md and docs/*.md executes in a generated Rails 8.1 app (`bin/rails generate
|
|
2253
|
+
authentication` + a scaffold `Customer` as generated, PermissionForge installed by the README's own install lines)
|
|
2254
|
+
on every test run, one console per document where nobody holds a set yet, with `# => literal` comments asserted;
|
|
2255
|
+
the denial flows of "Handling Denials" (a
|
|
2256
|
+
Turbo sign-in with no Permission Set, Reload, Sign out, the public index with and without `resume_session`, the
|
|
2257
|
+
sign-in page under the guard, README step 4 after an admin-made set, seeds under `:strict`) are driven as requests,
|
|
2258
|
+
and in Chrome with Turbo in the `browser` CI job (`test/browser/docs_flows.spec.js`).
|
|
2259
|
+
<!-- pin: test/permission_forge/docs/doc_samples_test.rb#every run and file= block of the documentation executes in the docs app, and every literal # => claim holds -->
|
|
2260
|
+
- [Handling denials](docs/row-level-rules.md#rescuing-them-yourself): the denial handler lives in one place, the
|
|
2261
|
+
complete `app/controllers/application_controller.rb` of a Rails 8 authentication-generator app;
|
|
2262
|
+
`docs/controller-actions.md` points to it. It renders `errors/forbidden` in the app's layout (with Sign out) and
|
|
2263
|
+
sends to sign in only a visitor with no session — never to the refused page (`redirect_back` looped on a refused
|
|
2264
|
+
root; a Turbo sign-in ending on a bare 403 reloaded into an empty form). `resume_session` ahead of the guard is
|
|
2265
|
+
required on a guarded public action; the `SessionsController` sample is the generator's file plus one `new` line.
|
|
2266
|
+
All of it is `file=` blocks the docs app applies.
|
|
2267
|
+
<!-- pin: test/permission_forge/docs/doc_blocks_test.rb#the five handler versions the rounds went through are refused -->
|
|
2268
|
+
<!-- pin: docs/row-level-rules.md#rescuing-them-yourself -->
|
|
2269
|
+
- `permission_scoped` / `find_by` apply the row-level rules only: the resource's CRUD flags are the guard's (REST
|
|
2270
|
+
actions → flags) or your own `can_read?` check; documented with a driven scenario (a set with a rule and no read:
|
|
2271
|
+
rows answered, the page refused).
|
|
2272
|
+
<!-- pin: test/permission_forge/docs/doc_samples_test.rb#every run and file= block of the documentation executes in the docs app, and every literal # => claim holds -->
|
|
2273
|
+
- README Installation and the getting-started guide mount the admin UI and set its credentials; without them there
|
|
2274
|
+
is no admin to open. The README Quick Start defines the `user` it assigns, says to run step 4's lines in order (a
|
|
2275
|
+
failing `create!` first), and step 5 looks up the person step 4 assigned instead of `User.first`.
|
|
2276
|
+
<!-- pin: README.md#installation -->
|
|
2277
|
+
- The initializer template, README and guides no longer call `controller.current_user` / `controller.current_admin` in
|
|
2278
|
+
the authorization examples, which Rails 8's generator does not define (a 500 on the default base controllers):
|
|
2279
|
+
they use `PermissionForge.current_user`, and the `audit_actor_resolver` examples show a Rails 8 and a Devise variant
|
|
2280
|
+
for HTTP Basic auth.
|
|
2281
|
+
<!-- pin: README.md#1-tell-it-who-is-signed-in -->
|
|
2282
|
+
- [Admin UI](docs/admin-ui.md#rails-8-authentication-generator): with Rails 8's generator as `base_controller_class`, a
|
|
2283
|
+
signed-out visit was a 500 (`new_session_path` does not resolve inside the engine); a complete admin base controller
|
|
2284
|
+
that redirects through `main_app` and checks for an admin. "API-only applications" sections in the admin UI and
|
|
2285
|
+
admin API guides.
|
|
2286
|
+
<!-- pin: docs/admin-ui.md#rails-8-authentication-generator -->
|
|
2287
|
+
- [Controller actions](docs/controller-actions.md#callback-order): the user must be resolved before the guard and any
|
|
2288
|
+
model read; an action opened with `allow_unauthenticated_access` needs `prepend_before_action :resume_session`.
|
|
2289
|
+
<!-- pin: docs/controller-actions.md#callback-order -->
|
|
2290
|
+
- [Configuration](docs/configuration.md#how-people-are-labelled-and-searched): how people are labelled, which columns
|
|
2291
|
+
are searched in SQL, and that an Array-returning `user_list_resolver` is searched in Ruby and loads every user.
|
|
2292
|
+
<!-- pin: docs/configuration.md#how-people-are-labelled-and-searched -->
|
|
2293
|
+
- [Configuration](docs/configuration.md#where-warnings-appear), Where warnings appear: the one channel policy of
|
|
2294
|
+
every boot and configuration warning.
|
|
2295
|
+
<!-- pin: docs/configuration.md#where-warnings-appear -->
|
|
2296
|
+
- [Admin API](docs/admin-api.md#response-format): a flat JSON body is accepted where Rails wraps parameters (only an
|
|
2297
|
+
empty, missing or non-object section answers 422); the collection sample shows `meta.pagination`; the condition
|
|
2298
|
+
shapes and their 422; the `RecordNotUnique` 422.
|
|
2299
|
+
<!-- pin: docs/admin-api.md#response-format -->
|
|
2300
|
+
- [Admin API](docs/admin-api.md#who-the-audit-log-names-for-an-api-change): why API changes read "API client" even
|
|
2301
|
+
from a signed-in browser (the default `api_base_controller_class` runs none of your sign-in, so `Current.user` is not
|
|
2302
|
+
set), and the two ways to name the caller: an `api_base_controller_class` that resumes the session, or an
|
|
2303
|
+
`audit_actor_resolver` that maps the credential `api_auth` verified (never a caller-chosen header).
|
|
2304
|
+
<!-- pin: docs/admin-api.md#who-the-audit-log-names-for-an-api-change -->
|
|
2305
|
+
- [Admin API](docs/admin-api.md#permission-set-group-users-group-membership--grants-nothing): the group-member
|
|
2306
|
+
endpoints say that membership is organisational and grants none of the group's sets.
|
|
2307
|
+
<!-- pin: docs/admin-api.md#permission-set-group-users-group-membership--grants-nothing -->
|
|
2308
|
+
- [Admin API](docs/admin-api.md#disabling-the-api-entirely): while `api_enabled` is off the empty 404 (not the JSON
|
|
2309
|
+
envelope) is deliberate: it does not reveal that the API exists.
|
|
2310
|
+
<!-- pin: docs/admin-api.md#disabling-the-api-entirely -->
|
|
2311
|
+
- [Assignment expiration](docs/assignment-expiration.md#rake-task-stale-assignments): `rake
|
|
2312
|
+
permission_forge:stale_assignments`, its `THRESHOLD_DAYS`, its output and its exit status 1 when it finds stale
|
|
2313
|
+
assignments.
|
|
2314
|
+
<!-- pin: docs/assignment-expiration.md#rake-task-stale-assignments -->
|
|
2315
|
+
- [Getting started](docs/getting-started.md#wiring-a-scaffold): `as_json` leaves out a field with a Hidden row, but a
|
|
2316
|
+
column with no row is serialized as `null` under the default `unconfigured_field_strategy = :block`; how to give
|
|
2317
|
+
every column a row. The jbuilder example's `only:` keeps the scaffold's columns, and the scaffold's HTML partial
|
|
2318
|
+
skips the fields the user may not read (a helper on `field_permissions`). `ResourceFieldGenerator` without
|
|
2319
|
+
`grant_all` leaves every column Hidden, `created_at` / `updated_at` and the personal-data columns included; only
|
|
2320
|
+
`grant_all` makes the timestamps read-only.
|
|
2321
|
+
<!-- pin: docs/getting-started.md#wiring-a-scaffold -->
|
|
2322
|
+
- [Getting started](docs/getting-started.md#using-rails-8s-authentication-generator): the symptoms of a guarded
|
|
2323
|
+
sign-in page (the 403 page where the form should be, also after Sign out) and of a public action without
|
|
2324
|
+
`resume_session`; steps 5 and 6 look up the person they assign by email instead of `User.first`, which may be
|
|
2325
|
+
someone holding no set; a seed's `find_or_create_by!` run with nobody signed in creates a duplicate on every run
|
|
2326
|
+
under `config.preset = :strict` (also in the generated initializer's comment).
|
|
2327
|
+
<!-- pin: docs/getting-started.md#using-rails-8s-authentication-generator -->
|
|
2328
|
+
- [Field filtering](docs/field-filtering.md#telling-the-user-what-was-not-saved): an unchanged form posts every field
|
|
2329
|
+
back, so "Not saved: …" named every field the user may not edit; the form renders such fields disabled (not
|
|
2330
|
+
submitted) and leaves out the ones they may not read.
|
|
2331
|
+
<!-- pin: docs/field-filtering.md#telling-the-user-what-was-not-saved -->
|
|
2332
|
+
- The generated initializer, README and [Presets](docs/configuration.md#presets): `config.preset = :strict` does not
|
|
2333
|
+
close every finding. Masking with nobody signed in becomes `:hide_in_requests` (jobs and the console keep the stored
|
|
2334
|
+
values: the audit notes `masked_nil_user_outside_requests`), and the checklist rows that need your code are in no
|
|
2335
|
+
preset.
|
|
2336
|
+
<!-- pin: docs/configuration.md#presets -->
|
|
2337
|
+
- [Without a current user](docs/masked-attributes.md#without-a-current-user): what each value of
|
|
2338
|
+
`masked_attributes_nil_user_strategy` does in a web request and elsewhere, what a web request is (and is not), the
|
|
2339
|
+
trade-off of `:hide_in_requests` and what `:bypass` records.
|
|
2340
|
+
<!-- pin: docs/masked-attributes.md#without-a-current-user -->
|
|
2341
|
+
- [filter_parameters_integration](docs/configuration.md#filter_parameters_integration) and the GDPR guide: the patterns
|
|
2342
|
+
filter every `name` parameter in the app's request logs, the admin's own forms included; how to narrow them. While
|
|
2343
|
+
on, the columns `filter_parameters` name count as personal data for the presets, Sync, the audits and the boot
|
|
2344
|
+
warning.
|
|
2345
|
+
<!-- pin: docs/configuration.md#filter_parameters_integration -->
|
|
2346
|
+
- [Activation conditions](docs/activation-conditions.md#ip-range-restrictions): IP lists (entries, separators, arrays
|
|
2347
|
+
through the API, refusal of entries that are not addresses, normalisation), hour windows (both hours or none;
|
|
2348
|
+
`start == end` refused; what a broken stored window does) and the model methods (`allowed_ip_list`, `hours_window`,
|
|
2349
|
+
`condition_problems`); [Security audit](docs/security-audit.md): the `conditions` check rows.
|
|
2350
|
+
<!-- pin: docs/activation-conditions.md#ip-range-restrictions -->
|
|
2351
|
+
- [I18n](docs/i18n.md#your-models-and-columns-in-the-admin): what "fully internationalized" means and how it is
|
|
2352
|
+
proven; translating `activerecord.models` / `activerecord.attributes` per locale for the admin's labels, else the
|
|
2353
|
+
row's label, else the stored name in code; [Column Defaults Are English](docs/i18n.md#column-defaults-are-english)
|
|
2354
|
+
(the install migration's defaults, the admin never pre-fills them, the blank-message denial, the one-liner for
|
|
2355
|
+
existing rows); the locale-file lints for contributors; the `denials` and `validations` key families.
|
|
2356
|
+
<!-- pin: docs/i18n.md#your-models-and-columns-in-the-admin -->
|
|
2357
|
+
- `rake permission_forge:erasure` is documented with `USER_ID`, `DRY_RUN`, `ACTOR` and `REASON`
|
|
2358
|
+
([GDPR data handling](docs/gdpr-data-handling.md#running-checks-without-claude)), and `rake
|
|
2359
|
+
permission_forge:rls:remove` by its full name next to `rls:sync` and `rls:status`
|
|
2360
|
+
([Query scoping](docs/query-scoping.md#how-the-policies-work)).
|
|
2361
|
+
<!-- pin: docs/query-scoping.md#how-the-policies-work -->
|
|
2362
|
+
- The wave-1 docs of this release applied: [Using the wrong field name](docs/row-level-rules.md#using-the-wrong-field-name)
|
|
2363
|
+
and the other rule problems (unknown operators and mask strategies, NUL bytes, values the column cannot hold —
|
|
2364
|
+
refused on save, matching nothing when stored), activation conditions, variants and the merge logic, the cache scope
|
|
2365
|
+
every operation reports, the `RecordNotUnique` 422, the pagination rule, the raw reader table of masked attributes,
|
|
2366
|
+
and phone width.
|
|
2367
|
+
<!-- pin: test/permission_forge/docs/references_test.rb#every relative link and anchor in the docs resolves -->
|
|
2368
|
+
- **Upgrading to 0.15 opens with a three-step checklist**, and the 0.13 path is stated once
|
|
2369
|
+
([From 0.13](docs/migration-guide.md#from-013)): `permission_forge:upgrade` is for installs older than 0.13, the
|
|
2370
|
+
audit runs before the deploy and RLS is re-synced after it; the README, this section's intro and its Removed entry
|
|
2371
|
+
link there (field test round 7, A#131). `docs/i18n.md` says what an English page shows for a host name without a
|
|
2372
|
+
translation ([Your Models and Columns in the Admin](docs/i18n.md#your-models-and-columns-in-the-admin)).
|
|
2373
|
+
<!-- pin: test/permission_forge/docs/upgrade_notes_test.rb#Upgrading to 0.15 opens with the three-step checklist -->
|
|
2374
|
+
<!-- pin: test/permission_forge/docs/upgrade_notes_test.rb#the 0.13 path is stated once, in From 0.13, and the rest link to it -->
|
|
2375
|
+
- [Wiring a scaffold](docs/getting-started.md#wiring-a-scaffold) says what to change in the scaffold's migration:
|
|
2376
|
+
`owner:references` writes `foreign_key: true`, a foreign key to a table `owners`, so the first insert failed on
|
|
2377
|
+
SQLite (`no such table: main.owners`); the guide points it at the users table (`foreign_key: { to_table: :users }`)
|
|
2378
|
+
(field test round 8, A#13).
|
|
2379
|
+
<!-- pin: docs/getting-started.md#wiring-a-scaffold -->
|
|
2380
|
+
|
|
9
2381
|
## [0.14.1] - 2026-10-14
|
|
10
2382
|
|
|
11
2383
|
### Security
|