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
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Cache
|
|
5
|
+
# What one cache-touching operation did to the cached permissions, as
|
|
6
|
+
# every surface reports it — the terminal (RakeOutput), the admin flash
|
|
7
|
+
# (CacheConfirmation), the API body and the summary audit event:
|
|
8
|
+
#
|
|
9
|
+
# - `confirmed`: true when the store confirmed every removal the
|
|
10
|
+
# operation asked for (Cache::Failure reported nothing), false when it
|
|
11
|
+
# did not (the old permissions may be served until cache_ttl), nil when
|
|
12
|
+
# that is not known where the outcome is made — the invalidation
|
|
13
|
+
# follows the surrounding transaction's commit (Cache::Settlement), or
|
|
14
|
+
# the summary event is written before the cache work runs;
|
|
15
|
+
# - `process_local`: the store lives in each process
|
|
16
|
+
# (Cache::Base#process_local?: `cache_backend = :memory`, or Rails.cache a
|
|
17
|
+
# `:memory_store`), so only the process that ran the operation saw it;
|
|
18
|
+
# - `in_request`: the operation ran in a server process (the admin or
|
|
19
|
+
# the API: Audit::Source 'admin_ui' / 'api', or any code of a web
|
|
20
|
+
# server process, a host controller among it: Report.in_request?), so
|
|
21
|
+
# on a process-local store that server process did see it and the
|
|
22
|
+
# *other* web workers and the job processes kept theirs; false for a
|
|
23
|
+
# rake task, a runner, a console or a job process, where every other
|
|
24
|
+
# process kept theirs;
|
|
25
|
+
# - `unconfirmed`: how many users the store did not confirm (nil when not
|
|
26
|
+
# known yet);
|
|
27
|
+
# - `work`: false when the operation did no cache work at all — a dry run
|
|
28
|
+
# (.none: `confirmed` nil, `unconfirmed` 0); true otherwise. Only an
|
|
29
|
+
# outcome without work carries the key in #to_h (`work: false`), so
|
|
30
|
+
# every answer of an operation that did its work keeps its four keys.
|
|
31
|
+
# A dry run used to answer `confirmed: true` for work it never did
|
|
32
|
+
# (field test round 7, E#55).
|
|
33
|
+
#
|
|
34
|
+
# Made by Cache::Report, the one way a service measures an invalidation.
|
|
35
|
+
#
|
|
36
|
+
# @api private
|
|
37
|
+
Outcome = Data.define(:confirmed, :process_local, :unconfirmed, :in_request, :work) do
|
|
38
|
+
# @param work [Boolean] false only for an operation that did no cache work (.none)
|
|
39
|
+
def initialize(confirmed:, process_local:, unconfirmed:, in_request:, work: true)
|
|
40
|
+
super
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# The outcome of an operation that did no cache work (every dry run):
|
|
44
|
+
# nothing asked of the store, so nothing confirmed and nobody left
|
|
45
|
+
# unconfirmed; the store's scope and the origin as measured.
|
|
46
|
+
#
|
|
47
|
+
# @param process_local [Boolean]
|
|
48
|
+
# @param in_request [Boolean]
|
|
49
|
+
# @return [Outcome]
|
|
50
|
+
def self.none(process_local:, in_request:)
|
|
51
|
+
new(confirmed: nil, process_local: process_local, unconfirmed: 0, in_request: in_request, work: false)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# The outcome before the cache work has run, or where its result is not
|
|
55
|
+
# known: nothing confirmed yet, the store's scope and the origin known
|
|
56
|
+
# already.
|
|
57
|
+
#
|
|
58
|
+
# @param process_local [Boolean]
|
|
59
|
+
# @param in_request [Boolean]
|
|
60
|
+
# @return [Outcome]
|
|
61
|
+
def self.deferred(process_local:, in_request:)
|
|
62
|
+
new(confirmed: nil, process_local: process_local, unconfirmed: nil, in_request: in_request)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# An Outcome from what a service answered under `cache:` (its Hash, or
|
|
66
|
+
# the Outcome itself).
|
|
67
|
+
#
|
|
68
|
+
# @param value [Outcome, Hash]
|
|
69
|
+
# @return [Outcome]
|
|
70
|
+
def self.wrap(value)
|
|
71
|
+
return value if value.is_a?(self)
|
|
72
|
+
|
|
73
|
+
hash = value.to_h.transform_keys(&:to_sym)
|
|
74
|
+
new(confirmed: hash[:confirmed], process_local: hash[:process_local] == true, unconfirmed: hash[:unconfirmed],
|
|
75
|
+
in_request: hash[:in_request] == true, work: hash[:work] != false)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# What a summary event records (string keys, as event data is stored).
|
|
79
|
+
#
|
|
80
|
+
# @return [Hash]
|
|
81
|
+
def event_data
|
|
82
|
+
{ 'cache_confirmed' => confirmed, 'process_local' => process_local, 'in_request' => in_request, 'unconfirmed' => unconfirmed }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def process_local? = process_local == true
|
|
86
|
+
def in_request? = in_request == true
|
|
87
|
+
def confirmed? = confirmed == true
|
|
88
|
+
# No cache work was done (.none: a dry run).
|
|
89
|
+
def none? = work == false
|
|
90
|
+
|
|
91
|
+
# The outcome as a service answers it: the four keys, and `work: false`
|
|
92
|
+
# for an operation that did no cache work (#none?).
|
|
93
|
+
#
|
|
94
|
+
# @return [Hash]
|
|
95
|
+
def to_h
|
|
96
|
+
none? ? super : super.except(:work)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# A flash: an alert only when the store said no; a confirmation that is
|
|
100
|
+
# not known yet is not a failure to announce.
|
|
101
|
+
#
|
|
102
|
+
# @return [Symbol] :notice or :alert
|
|
103
|
+
def flash_kind = confirmed == false ? :alert : :notice
|
|
104
|
+
|
|
105
|
+
# Which processes kept their cached permissions: `:none` (a shared
|
|
106
|
+
# store), `:other_processes` (a process-local store, the operation ran
|
|
107
|
+
# in a server process: the other web workers and the job processes),
|
|
108
|
+
# `:servers` (a process-local store, the operation ran elsewhere: every
|
|
109
|
+
# web and job process).
|
|
110
|
+
#
|
|
111
|
+
# @return [Symbol]
|
|
112
|
+
def kept_by
|
|
113
|
+
return :none unless process_local?
|
|
114
|
+
|
|
115
|
+
in_request? ? :other_processes : :servers
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -83,6 +83,12 @@ module PermissionForge
|
|
|
83
83
|
cache_store.is_a?(ActiveSupport::Cache::MemoryStore)
|
|
84
84
|
end
|
|
85
85
|
|
|
86
|
+
# A :null_store keeps nothing (the Rails 7.1/7.2 development default
|
|
87
|
+
# without `bin/rails dev:cache`).
|
|
88
|
+
def keeps_entries?
|
|
89
|
+
!cache_store.is_a?(ActiveSupport::Cache::NullStore)
|
|
90
|
+
end
|
|
91
|
+
|
|
86
92
|
# An explicit `expires_in: nil` overrides the store's default expiry.
|
|
87
93
|
def write_permanent(key, value)
|
|
88
94
|
cache_store.write(prefixed_key(key), value, expires_in: nil)
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'outcome'
|
|
4
|
+
|
|
5
|
+
module PermissionForge
|
|
6
|
+
module Cache
|
|
7
|
+
# The one way a service measures the cache work it does: runs the block
|
|
8
|
+
# under Cache::Failure.collect (so a host's override of
|
|
9
|
+
# invalidate_permissions decides nothing), asks the store once whether it
|
|
10
|
+
# is process-local, and answers a Cache::Outcome.
|
|
11
|
+
#
|
|
12
|
+
# outcome = Cache::Report.of { user.invalidate_permissions } # one user
|
|
13
|
+
# outcome = Cache::Report.of(user_ids: ids) { ids.each { ... } } # several: unconfirmed counted per user
|
|
14
|
+
#
|
|
15
|
+
# Measured inside a caller's transaction the outcome is pending
|
|
16
|
+
# (#deferred?, the one reading): the invalidations follow its commit.
|
|
17
|
+
#
|
|
18
|
+
# report = Cache::Report.new # when the summary event is written inside the measured block
|
|
19
|
+
# report.pending.event_data # the scope known in advance, nothing confirmed yet
|
|
20
|
+
# report.measure { ... }.value
|
|
21
|
+
# report.outcome(user_ids: ids)
|
|
22
|
+
#
|
|
23
|
+
# @api private
|
|
24
|
+
class Report
|
|
25
|
+
# @return [Object] what the measured block returned
|
|
26
|
+
attr_reader :value
|
|
27
|
+
|
|
28
|
+
class << self
|
|
29
|
+
# Measures the block's cache work.
|
|
30
|
+
#
|
|
31
|
+
# @param user_ids [Array, nil] the users invalidated (nil: one, unnamed)
|
|
32
|
+
# @return [Outcome] pending (#deferred?) inside a caller's transaction
|
|
33
|
+
def of(user_ids: nil, &)
|
|
34
|
+
measure(&).outcome(user_ids: user_ids)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @return [Report] measured; `value` holds what the block returned
|
|
38
|
+
def measure(&)
|
|
39
|
+
new.measure(&)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Runs the block, collecting the cache failures reported inside it.
|
|
44
|
+
# Whether the work waits for a caller's transaction is read here, once,
|
|
45
|
+
# before the first block (#deferred?).
|
|
46
|
+
#
|
|
47
|
+
# @return [Report] self
|
|
48
|
+
def measure
|
|
49
|
+
@deferred = PermissionForge::OpenTransaction.current ? true : false if @deferred.nil?
|
|
50
|
+
@collected = Failure.collect { @value = yield }
|
|
51
|
+
self
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# The one reading of "deferred" for every operation that measures its
|
|
55
|
+
# cache work: a caller's transaction was open when it was measured
|
|
56
|
+
# (OpenTransaction.current), so the invalidations follow its commit
|
|
57
|
+
# (the rows' own after_commit, Cache::Settlement) and nothing is
|
|
58
|
+
# confirmed yet. False before anything was measured.
|
|
59
|
+
#
|
|
60
|
+
# @return [Boolean]
|
|
61
|
+
def deferred? = @deferred == true
|
|
62
|
+
|
|
63
|
+
# Whether the store lives in this process only — asked once per report.
|
|
64
|
+
#
|
|
65
|
+
# @return [Boolean]
|
|
66
|
+
def process_local?
|
|
67
|
+
@process_local = PermissionForge.cache.process_local? if @process_local.nil?
|
|
68
|
+
@process_local
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Whether the operation runs in a server process (Report.in_request?).
|
|
72
|
+
#
|
|
73
|
+
# @return [Boolean]
|
|
74
|
+
def in_request?
|
|
75
|
+
@in_request = Report.in_request? if @in_request.nil?
|
|
76
|
+
@in_request
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# The one definition of "the operation ran in a server process": the
|
|
80
|
+
# audit context says it came from the admin or the API, or this process
|
|
81
|
+
# is a web server (Warnings.server_process?: a host controller, or a
|
|
82
|
+
# job run inline by Puma, whatever its audit source). A rake task, a
|
|
83
|
+
# runner, a console or a job process is a process of its own.
|
|
84
|
+
#
|
|
85
|
+
# @return [Boolean]
|
|
86
|
+
def self.in_request?
|
|
87
|
+
REQUEST_SOURCES.include?(PermissionForge::Audit::Source.current) || PermissionForge::Warnings.server_process?
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# The audit sources of a request a server process serves.
|
|
91
|
+
REQUEST_SOURCES = %w[admin_ui api].freeze
|
|
92
|
+
|
|
93
|
+
# Those of `user_ids` whose invalidation the store confirmed.
|
|
94
|
+
#
|
|
95
|
+
# @param user_ids [Array]
|
|
96
|
+
# @return [Array]
|
|
97
|
+
def confirmed(user_ids)
|
|
98
|
+
collected.confirmed(user_ids)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# The outcome before the measurement (the summary event of a service
|
|
102
|
+
# that invalidates after writing it records this).
|
|
103
|
+
#
|
|
104
|
+
# @return [Outcome]
|
|
105
|
+
def pending
|
|
106
|
+
Outcome.deferred(process_local: process_local?, in_request: in_request?)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# The outcome of an operation that does no cache work (a dry run):
|
|
110
|
+
# Outcome.none, with the store's scope and the origin as this report
|
|
111
|
+
# reads them.
|
|
112
|
+
#
|
|
113
|
+
# @return [Outcome]
|
|
114
|
+
def none
|
|
115
|
+
Outcome.none(process_local: process_local?, in_request: in_request?)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# The outcome of a dry run, measured nowhere (Report#none).
|
|
119
|
+
#
|
|
120
|
+
# @return [Outcome]
|
|
121
|
+
def self.none
|
|
122
|
+
new.none
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# @param user_ids [Array, nil] the users invalidated, to count the
|
|
126
|
+
# unconfirmed ones (nil: one unnamed — any failure counts as one)
|
|
127
|
+
# @param unconfirmed [Integer, nil] a count the caller made itself
|
|
128
|
+
# (PermissionCacheClearer counts by slice)
|
|
129
|
+
# @return [Outcome] pending while #deferred?
|
|
130
|
+
def outcome(user_ids: nil, unconfirmed: nil)
|
|
131
|
+
return pending if deferred?
|
|
132
|
+
|
|
133
|
+
unconfirmed ||= count_unconfirmed(user_ids)
|
|
134
|
+
Outcome.new(confirmed: unconfirmed.zero?, process_local: process_local?, unconfirmed: unconfirmed, in_request: in_request?)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
private
|
|
138
|
+
|
|
139
|
+
def collected
|
|
140
|
+
@collected || Failure::Collected.new
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def count_unconfirmed(user_ids)
|
|
144
|
+
return user_ids.size - confirmed(user_ids).size if user_ids
|
|
145
|
+
|
|
146
|
+
collected.to_a.empty? ? 0 : 1
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -15,7 +15,9 @@ module PermissionForge
|
|
|
15
15
|
# another version never reads an entry it would misread. An invalidation
|
|
16
16
|
# removes the keys of the previous namespaces as well
|
|
17
17
|
# (PREVIOUS_ENTRY_NAMESPACES), so a change made through either version
|
|
18
|
-
# reaches both
|
|
18
|
+
# reaches both — for one minor version after a bump (0.15 dropped the
|
|
19
|
+
# bare keys of 0.13; none is listed now). The stamp is not namespaced:
|
|
20
|
+
# every version must see it.
|
|
19
21
|
#
|
|
20
22
|
# An invalidation removes the base entries, and reads nothing: the
|
|
21
23
|
# per-context variants of a user with conditional sets cannot be reached
|
|
@@ -30,8 +32,10 @@ module PermissionForge
|
|
|
30
32
|
# @api private
|
|
31
33
|
module UserKeys
|
|
32
34
|
ENTRY_NAMESPACE = 'v2'
|
|
33
|
-
#
|
|
34
|
-
|
|
35
|
+
# The namespaces of the previous entry format, removed by every
|
|
36
|
+
# invalidation for one minor version after a bump ('' would be a bare
|
|
37
|
+
# key). Empty since 0.15, which dropped the bare keys of 0.13.
|
|
38
|
+
PREVIOUS_ENTRY_NAMESPACES = [].freeze
|
|
35
39
|
|
|
36
40
|
module_function
|
|
37
41
|
|
|
@@ -118,7 +122,7 @@ module PermissionForge
|
|
|
118
122
|
[current, previous]
|
|
119
123
|
end
|
|
120
124
|
|
|
121
|
-
# `key`, of this namespace, in another one ('' :
|
|
125
|
+
# `key`, of this namespace, in another one ('' : a bare key).
|
|
122
126
|
def in_namespace(key, namespace)
|
|
123
127
|
bare = key.delete_prefix("#{ENTRY_NAMESPACE}:")
|
|
124
128
|
namespace.empty? ? bare : "#{namespace}:#{bare}"
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
# Boot warnings about a cache store that does not reach other processes.
|
|
5
|
+
# Called by BootChecks once per boot; only the configuration and the
|
|
6
|
+
# store's class are read. Each warning goes through Warnings.emit, which
|
|
7
|
+
# decides the channel (in development the terminal as well as the log:
|
|
8
|
+
# the boot logs before `bin/rails server` attaches its console logger, and
|
|
9
|
+
# `bin/rails runner`, the console and `db:seed` never do).
|
|
10
|
+
#
|
|
11
|
+
# A store of its own in every process (Cache::Base#process_local?:
|
|
12
|
+
# cache_backend = :memory, or Rails.cache / a store instance that is an
|
|
13
|
+
# ActiveSupport MemoryStore): an invalidation reaches the cache of the
|
|
14
|
+
# process that makes it, so a change made elsewhere (a console,
|
|
15
|
+
# `bin/rails runner`, a rake task, another Puma worker) leaves this
|
|
16
|
+
# process's cached permission hashes as they were until cache_ttl.
|
|
17
|
+
#
|
|
18
|
+
# Such a store, or one that keeps nothing (Cache::Base#keeps_entries?:
|
|
19
|
+
# cache_backend = :null, a :null_store), also cannot carry the token
|
|
20
|
+
# MaskingSync announces a newly masked model with, while a masking setting
|
|
21
|
+
# takes its models from the Resource rows (MaskedModels.row_driven?). In
|
|
22
|
+
# development MaskingSync re-reads the Resource names from the database
|
|
23
|
+
# instead (MaskingSync.database_recheck?); elsewhere a model first named in
|
|
24
|
+
# another process stays unmasked here until a restart.
|
|
25
|
+
#
|
|
26
|
+
# @api private
|
|
27
|
+
module CacheWarnings
|
|
28
|
+
# The remedies for masking that cannot be announced, in every warning.
|
|
29
|
+
MASKING_REMEDIES = 'Use a cache store every process shares (Redis, Memcached, Solid Cache; ' \
|
|
30
|
+
'in development also :file_store), or include PermissionForge::Concerns::MaskedAttributes ' \
|
|
31
|
+
'in each model that holds personal data and set config.auto_apply_masked_attributes = false ' \
|
|
32
|
+
'(the recommended production setup), or list the models in ' \
|
|
33
|
+
'config.auto_apply_masked_attributes. See docs/masked-attributes.md.'
|
|
34
|
+
|
|
35
|
+
# What store_description adds about a :memory_store in development.
|
|
36
|
+
DEVELOPMENT_DEFAULT = ', the Rails development default'
|
|
37
|
+
|
|
38
|
+
module_function
|
|
39
|
+
|
|
40
|
+
# @param config [Configuration]
|
|
41
|
+
# @return [void]
|
|
42
|
+
def call(config)
|
|
43
|
+
if Rails.env.production?
|
|
44
|
+
warn_in_production(config)
|
|
45
|
+
elsif Rails.env.development?
|
|
46
|
+
warn_in_development(config)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def warn_in_production(config)
|
|
51
|
+
cache = PermissionForge.cache
|
|
52
|
+
if cache.process_local?
|
|
53
|
+
Warnings.emit(:cache_process_local,
|
|
54
|
+
'Using an in-process permission cache in production (cache_backend = :memory, or ' \
|
|
55
|
+
'Rails.cache is a :memory_store). Permissions will be inconsistent across Puma workers: a ' \
|
|
56
|
+
'revoked permission stays in use in the other workers until cache_ttl. ' \
|
|
57
|
+
'Configure config.cache_backend = :rails_cache with Redis, Memcached, or SolidCache.', severity: :security)
|
|
58
|
+
end
|
|
59
|
+
return unless unannounced_masking?(cache)
|
|
60
|
+
|
|
61
|
+
Warnings.emit(:cache_unannounced_masking,
|
|
62
|
+
"#{masking_setting} takes its models from the Resource rows, but the cache store is " \
|
|
63
|
+
"#{store_description(config, cache)}: it cannot tell the other processes that a Resource row " \
|
|
64
|
+
'names a new model. A model first named by a row committed in another process (another ' \
|
|
65
|
+
'worker, a console, a rake task) stays unmasked in this process until it restarts. ' \
|
|
66
|
+
"#{MASKING_REMEDIES}", severity: :security)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Rails 8.0+ generates `config.cache_store = :memory_store` for
|
|
70
|
+
# development (7.1/7.2: :memory_store after `bin/rails dev:cache`,
|
|
71
|
+
# :null_store otherwise), and the generated initializer caches through
|
|
72
|
+
# Rails.cache.
|
|
73
|
+
def warn_in_development(config)
|
|
74
|
+
cache = PermissionForge.cache
|
|
75
|
+
Warnings.emit(:cache_stale_permissions, stale_permissions_warning(config)) if cache.process_local?
|
|
76
|
+
Warnings.emit(:cache_development_masking, development_masking_warning(config, cache)) if unannounced_masking?(cache)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Printed by every development process that boots (a server, a console,
|
|
80
|
+
# `bin/rails runner`, `db:seed`), so it speaks of processes, not of
|
|
81
|
+
# "this server".
|
|
82
|
+
def stale_permissions_warning(config)
|
|
83
|
+
"Development: the permission cache is #{store_description(config, PermissionForge.cache)}, " \
|
|
84
|
+
'one per process. A permission change made in one process (a console, `bin/rails runner`, ' \
|
|
85
|
+
"a rake task or db:seed) invalidates only that process's cache: a running server keeps the " \
|
|
86
|
+
"permissions it has cached until cache_ttl (#{config.cache_ttl.inspect} s) or a restart. " \
|
|
87
|
+
"Changes made in the server's own admin UI apply there at once. To share the cache between " \
|
|
88
|
+
'processes, set config.cache_store = :file_store, "tmp/cache/store" in ' \
|
|
89
|
+
'config/environments/development.rb (with config.cache_backend = :rails_cache), or set ' \
|
|
90
|
+
'config.cache_backend = :null in development (no caching). See docs/configuration.md#cache-consistency.'
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def development_masking_warning(config, cache)
|
|
94
|
+
"Development: #{masking_setting} takes its models from the Resource rows, and the cache " \
|
|
95
|
+
"store is #{store_description(config, cache)}: it cannot tell a running server that a Resource row " \
|
|
96
|
+
'committed from a console, `bin/rails runner` or a rake task names a new model. In development ' \
|
|
97
|
+
'the server re-reads the Resource names from the database instead (one query at most every ' \
|
|
98
|
+
"#{MaskingSync::DATABASE_RECHECK} s); in production a process would mask such a model only after a " \
|
|
99
|
+
"restart. #{MASKING_REMEDIES}"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# A row-driven masking setting with a store that cannot carry
|
|
103
|
+
# MaskingSync's token to another process.
|
|
104
|
+
def unannounced_masking?(cache)
|
|
105
|
+
MaskedModels.row_driven? && (cache.process_local? || !cache.keeps_entries?)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def masking_setting
|
|
109
|
+
config = PermissionForge.configuration
|
|
110
|
+
names = { 'auto_apply_masked_attributes' => config.auto_apply_masked_attributes,
|
|
111
|
+
'mask_virtual_attributes' => config.mask_virtual_attributes }
|
|
112
|
+
on = names.reject { |_name, value| MaskedModels::OFF.include?(value) || value.is_a?(Array) }
|
|
113
|
+
"auto-applied masking (#{on.map { |name, value| "#{name} = #{value.inspect}" }.join(', ')})"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# How the warnings name the store. A :memory_store is what Rails
|
|
117
|
+
# generates for development (DEVELOPMENT_DEFAULT): said in a development
|
|
118
|
+
# boot only — a production boot with one has chosen it (field test
|
|
119
|
+
# round 8, E#66).
|
|
120
|
+
#
|
|
121
|
+
# @param env [ActiveSupport::EnvironmentInquirer] the environment booting
|
|
122
|
+
def store_description(config, cache, env: Rails.env)
|
|
123
|
+
return kept_nothing_description(config) unless cache.keeps_entries?
|
|
124
|
+
|
|
125
|
+
case config.cache_backend
|
|
126
|
+
when :memory then 'in memory (cache_backend = :memory)'
|
|
127
|
+
when :rails_cache then "an in-memory store (Rails.cache is a :memory_store#{DEVELOPMENT_DEFAULT if env.development?})"
|
|
128
|
+
else 'an in-memory store (cache_backend is an ActiveSupport::Cache::MemoryStore)'
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def kept_nothing_description(config)
|
|
133
|
+
case config.cache_backend
|
|
134
|
+
when :null then 'one that keeps nothing (cache_backend = :null)'
|
|
135
|
+
when :rails_cache then 'one that keeps nothing (Rails.cache is a :null_store)'
|
|
136
|
+
else 'one that keeps nothing (cache_backend is an ActiveSupport::Cache::NullStore)'
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -107,6 +107,7 @@ module PermissionForge
|
|
|
107
107
|
def pf_build_auto_scope
|
|
108
108
|
current_user = PermissionForge.current_user
|
|
109
109
|
return pf_nil_user_auto_scope unless current_user
|
|
110
|
+
return pf_no_read_access_auto_scope(current_user) if PermissionForge::RecordReadAccess.denied?(current_user, self)
|
|
110
111
|
|
|
111
112
|
# Resolve the rules once — permissions_hash is a cache read per
|
|
112
113
|
# call, and this hook runs on every relation build: those of this
|
|
@@ -130,6 +131,17 @@ module PermissionForge
|
|
|
130
131
|
)
|
|
131
132
|
end
|
|
132
133
|
|
|
134
|
+
# A signed-in user the model's Resource chain grants no read_access,
|
|
135
|
+
# under no_read_access_strategy :deny (RecordReadAccess): no rows,
|
|
136
|
+
# access.denied once per request.
|
|
137
|
+
def pf_no_read_access_auto_scope(current_user)
|
|
138
|
+
PermissionForge::Audit::Runtime.denied(
|
|
139
|
+
PermissionForge::Audit::Runtime::DENIED,
|
|
140
|
+
reason: PermissionForge::RecordReadAccess::REASON, model: self, via: 'auto_scope', user: current_user, once: true
|
|
141
|
+
)
|
|
142
|
+
none
|
|
143
|
+
end
|
|
144
|
+
|
|
133
145
|
# Fail closed for :deny AND :raise — raising from inside a default_scope
|
|
134
146
|
# would detonate in consoles, rake tasks, and view-layer counts, so the
|
|
135
147
|
# strictest safe behavior is an empty relation (documented deviation).
|
|
@@ -148,9 +160,14 @@ module PermissionForge
|
|
|
148
160
|
# Filter-type rules only: join-type rules inside a default_scope would
|
|
149
161
|
# break includes/count/pluck and duplicate rows, so they remain the job
|
|
150
162
|
# of permission_scoped (a boot warning fires when join rules exist while
|
|
151
|
-
# Level 1 is enabled).
|
|
163
|
+
# Level 1 is enabled). A join type this version does not know joins
|
|
164
|
+
# nothing — the applier makes such a rule match nothing — so it applies
|
|
165
|
+
# here too: skipped, it would leave the default scope wider than
|
|
166
|
+
# find_by and permission_scoped.
|
|
152
167
|
def pf_filter_rule?(rule)
|
|
153
|
-
|
|
168
|
+
join_type = rule[:join_type]
|
|
169
|
+
PermissionForge::PermissionSetRule.filter_join_type?(join_type) ||
|
|
170
|
+
!PermissionForge::Services::PermissionRuleApplier::JOIN_METHODS.key?(join_type.to_s)
|
|
154
171
|
end
|
|
155
172
|
end
|
|
156
173
|
end
|
|
@@ -76,7 +76,7 @@ module PermissionForge
|
|
|
76
76
|
return all unless config.tenant_enabled? && tenant_scoping_applied?
|
|
77
77
|
|
|
78
78
|
column = config.resolved_tenant_column
|
|
79
|
-
return unscoped if tenant_id.nil? || !
|
|
79
|
+
return unscoped if tenant_id.nil? || !PermissionForge::SchemaRead.column?(self, column.to_s, ignored: false)
|
|
80
80
|
|
|
81
81
|
unscoped.where(column => [tenant_id, nil])
|
|
82
82
|
end
|
|
@@ -11,7 +11,7 @@ module PermissionForge
|
|
|
11
11
|
included do
|
|
12
12
|
before_create :generate_uuid, if: -> { respond_to?(:uuid) && uuid.blank? }
|
|
13
13
|
|
|
14
|
-
validates :uuid, uniqueness:
|
|
14
|
+
validates :uuid, uniqueness: { message: ->(*) { I18n.t('permission_forge.validations.taken') } }, allow_nil: true
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
private
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Concerns
|
|
5
|
+
# A copy (Services::Clonable: `record.dup`) names what its original names.
|
|
6
|
+
# The checks a row gets when it is new or when an attribute changes —
|
|
7
|
+
# an operator or join type this version knows, a mask strategy that is
|
|
8
|
+
# one, an IP list of addresses — would refuse a copy of a row stored
|
|
9
|
+
# before them (or written past the model), and with it the whole copy
|
|
10
|
+
# of the set: a 500 in the admin's Duplicate. The copy keeps its
|
|
11
|
+
# original's stored values instead and behaves exactly like it (the rule
|
|
12
|
+
# matches nothing, the field masks fully, the condition never passes);
|
|
13
|
+
# `rake permission_forge:audit` reports both rows.
|
|
14
|
+
#
|
|
15
|
+
# keeps_copied :operator, :join_type
|
|
16
|
+
# validates :operator, ..., if: -> { !copied_unchanged?(:operator) && (new_record? || ...) }
|
|
17
|
+
#
|
|
18
|
+
# @api private
|
|
19
|
+
module KeepsCopiedValues
|
|
20
|
+
extend ActiveSupport::Concern
|
|
21
|
+
|
|
22
|
+
included do
|
|
23
|
+
class_attribute :pf_copied_attributes, instance_writer: false, default: [].freeze
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class_methods do
|
|
27
|
+
# @param attributes [Array<Symbol>] the attributes a copy keeps unchecked while it holds the original's value
|
|
28
|
+
# @return [void]
|
|
29
|
+
def keeps_copied(*attributes)
|
|
30
|
+
self.pf_copied_attributes = (pf_copied_attributes + attributes.map(&:to_s)).uniq.freeze
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def initialize_dup(other)
|
|
35
|
+
super
|
|
36
|
+
@pf_copied = attributes.slice(*pf_copied_attributes).freeze
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
# Whether this row is a copy and each attribute still holds what the
|
|
42
|
+
# original held: a check-on-change validation is not due for it.
|
|
43
|
+
def copied_unchanged?(*names)
|
|
44
|
+
!@pf_copied.nil? && names.all? { |name| @pf_copied.key?(name.to_s) && @pf_copied[name.to_s] == self[name] }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -36,8 +36,10 @@ module PermissionForge
|
|
|
36
36
|
# The module a class's alias readers and writers live in.
|
|
37
37
|
class AccessorModule < Module; end
|
|
38
38
|
|
|
39
|
-
# `id_value`, which Rails itself aliases to `id
|
|
40
|
-
#
|
|
39
|
+
# `id_value`, which Rails itself aliases to `id` (every model up to
|
|
40
|
+
# Rails 8.1; from 8.2 only a model with a composite key, the others
|
|
41
|
+
# get a plain reader), stays Rails': `id` is the primary key reader,
|
|
42
|
+
# not the reader of a column called id.
|
|
41
43
|
UNMASKED_TARGET = 'id'
|
|
42
44
|
|
|
43
45
|
CREATION = Mutex.new
|
|
@@ -61,11 +63,19 @@ module PermissionForge
|
|
|
61
63
|
accessors.redefine_method(:"#{name}=", &writer(klass, name, declared))
|
|
62
64
|
end
|
|
63
65
|
|
|
66
|
+
# The unmasked read is the one Rails' own alias reader makes: on
|
|
67
|
+
# Rails 8.2 `_read_attribute!` (it raises MissingAttributeError for
|
|
68
|
+
# an attribute the query did not load; `_read_attribute` takes no
|
|
69
|
+
# block there), before it `_read_attribute` with the block that
|
|
70
|
+
# raises. Which one is decided once, when the accessor is defined.
|
|
64
71
|
def self.reader(klass, name, declared)
|
|
72
|
+
raising = klass.method_defined?(:_read_attribute!) || klass.private_method_defined?(:_read_attribute!)
|
|
65
73
|
proc do
|
|
66
74
|
target = klass.attribute_aliases.fetch(name, declared)
|
|
67
75
|
if is_a?(MaskedAttributes) && pf_masked_reader?(target)
|
|
68
76
|
read_attribute(target) { |missing| missing_attribute(missing, caller) }
|
|
77
|
+
elsif raising
|
|
78
|
+
_read_attribute!(target)
|
|
69
79
|
else
|
|
70
80
|
_read_attribute(target) { |missing| missing_attribute(missing, caller) }
|
|
71
81
|
end
|
|
@@ -102,14 +102,16 @@ module PermissionForge
|
|
|
102
102
|
|
|
103
103
|
def pf_warn_virtual_readers_unmasked(error)
|
|
104
104
|
method = PermissionForge.configuration.extra_fields_method
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
PermissionForge::Warnings.emit("virtual_readers.unmasked.#{name}",
|
|
106
|
+
"the virtual readers #{name} declares are NOT masked on the model: " \
|
|
107
|
+
"#{name}.#{method} raised #{error.class}: #{error.message}. Fix it and " \
|
|
108
|
+
'restart, or call pf_mask_virtual_readers with explicit names.',
|
|
109
|
+
severity: :security)
|
|
108
110
|
end
|
|
109
111
|
|
|
110
112
|
def pf_virtual_reader?(name)
|
|
111
113
|
return false unless name.match?(READER_NAME)
|
|
112
|
-
return false if Array(primary_key).map(&:to_s).include?(name) ||
|
|
114
|
+
return false if Array(primary_key).map(&:to_s).include?(name) || PermissionForge::SchemaRead.column?(self, name, ignored: false)
|
|
113
115
|
|
|
114
116
|
attribute_names.include?(name) || (!pf_framework_method?(name) && pf_argumentless_reader?(name))
|
|
115
117
|
end
|