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,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
# Whether a ResourceField name names something on the Resource's model —
|
|
5
|
+
# what `rake permission_forge:audit` reports (integrity/unknown_fields): a
|
|
6
|
+
# typo such as `emial` configures nothing the model has. Saving does not
|
|
7
|
+
# refuse such a name: FiltersResponseAttributes filters any key of a hash
|
|
8
|
+
# (a key only a serializer computes is configured by a row of its name).
|
|
9
|
+
# A name is a field of the model when it is
|
|
10
|
+
#
|
|
11
|
+
# - a column of its table (a model's `ignored_columns` included), read
|
|
12
|
+
# again from the database before the name is called unknown
|
|
13
|
+
# (PermissionForge::SchemaRead): the schema cache may predate a column a
|
|
14
|
+
# migration just added;
|
|
15
|
+
# - an attribute (`attribute :nickname`) or an `alias_attribute`;
|
|
16
|
+
# - a virtual field: declared (extra_fields_method, also a key only a
|
|
17
|
+
# serializer computes) or discovered (store accessors, attribute-API
|
|
18
|
+
# attributes) — PermissionForge::VirtualFields;
|
|
19
|
+
# - a public reader or writer (a store accessor with a custom prefix, a
|
|
20
|
+
# `delegate … prefix: true`, `has_secure_password`'s `password=`),
|
|
21
|
+
# as FieldVariants counts one; or
|
|
22
|
+
# - a configured variant (config.field_variant_mappings) of one of those
|
|
23
|
+
# or of another row of the Resource: `owner_uuid` next to `owner_id`,
|
|
24
|
+
# the key a serializer computes and FiltersResponseAttributes reads by
|
|
25
|
+
# its base field (FieldVariants.base_entries).
|
|
26
|
+
#
|
|
27
|
+
# Any of those on a loaded single-table-inheritance subclass of the model
|
|
28
|
+
# (one on the same table) counts too: a row on the base Resource reaches
|
|
29
|
+
# the subclass's records (ResourceChain), so `vip_level`, a method only
|
|
30
|
+
# VipCustomer defines, is a field of Customer's rows. A subclass not
|
|
31
|
+
# loaded yet (development, before it is referenced) is not seen.
|
|
32
|
+
#
|
|
33
|
+
# Nothing is said (.for returns nil) when the model cannot be told: the
|
|
34
|
+
# Resource names no Active Record model this app can load (a Resource
|
|
35
|
+
# shared with another app), an abstract class, a table that does not exist
|
|
36
|
+
# (yet), or a database that cannot be asked.
|
|
37
|
+
#
|
|
38
|
+
# @example
|
|
39
|
+
# names = PermissionForge::FieldNames.for('Customer')
|
|
40
|
+
# names.include?('email') # => true
|
|
41
|
+
# names.include?('emial') # => false
|
|
42
|
+
#
|
|
43
|
+
# @api private
|
|
44
|
+
class FieldNames
|
|
45
|
+
# Errors that mean the model cannot be told now.
|
|
46
|
+
UNREADABLE = [NameError, ArgumentError, ActiveRecord::ActiveRecordError].freeze
|
|
47
|
+
|
|
48
|
+
# @param resource_name [String, Symbol, nil] the Resource's name
|
|
49
|
+
# @param siblings [Array<String>, #call] the names of the Resource's other
|
|
50
|
+
# rows (variant bases), or what answers them (asked only when needed)
|
|
51
|
+
# @return [FieldNames, nil] nil when the model cannot be told
|
|
52
|
+
def self.for(resource_name, siblings: [], configuration: PermissionForge.configuration)
|
|
53
|
+
model = ModelLookup.model_for(resource_name)
|
|
54
|
+
return unless model
|
|
55
|
+
|
|
56
|
+
new(model, siblings: siblings, configuration: configuration)
|
|
57
|
+
rescue *UNREADABLE
|
|
58
|
+
nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# An attribute, an alias, or a public reader or writer of the model —
|
|
62
|
+
# the one answer to "the model has this name" (FieldNames, FieldVariants).
|
|
63
|
+
# A name the table has no column for is asked again after the schema is
|
|
64
|
+
# read again, once (PermissionForge::SchemaRead.has?): a column a
|
|
65
|
+
# migration just added is an attribute.
|
|
66
|
+
#
|
|
67
|
+
# @param model [Class]
|
|
68
|
+
# @param name [String]
|
|
69
|
+
# @return [Boolean]
|
|
70
|
+
def self.declares?(model, name)
|
|
71
|
+
name = name.to_s
|
|
72
|
+
SchemaRead.has?(model, name) do
|
|
73
|
+
model.attribute_names.include?(name) || model.attribute_alias?(name) || model.public_method_defined?(name) ||
|
|
74
|
+
model.public_method_defined?("#{name}=")
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# @return [Class] the Resource's model
|
|
79
|
+
attr_reader :model
|
|
80
|
+
|
|
81
|
+
def initialize(model, siblings: [], configuration: PermissionForge.configuration)
|
|
82
|
+
@model = model
|
|
83
|
+
@siblings = siblings
|
|
84
|
+
@configuration = configuration
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# @param name [String, Symbol]
|
|
88
|
+
# @return [Boolean] whether the model has the name (its table read again,
|
|
89
|
+
# once, before saying no: PermissionForge::SchemaRead)
|
|
90
|
+
def include?(name)
|
|
91
|
+
name = name.to_s
|
|
92
|
+
SchemaRead.has?(@model, name) { known?(name) }
|
|
93
|
+
rescue *UNREADABLE
|
|
94
|
+
true
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def known?(name)
|
|
100
|
+
forget_stale_memos
|
|
101
|
+
own?(name) || variant?(name)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# The subclasses and virtual fields as of the schema the cache holds now:
|
|
105
|
+
# a re-read (SchemaRead) resets the model's attributes.
|
|
106
|
+
def forget_stale_memos
|
|
107
|
+
reads = SchemaRead.rereads
|
|
108
|
+
return if @reads == reads
|
|
109
|
+
|
|
110
|
+
@reads = reads
|
|
111
|
+
@virtual_fields = nil
|
|
112
|
+
@models = nil
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def own?(name)
|
|
116
|
+
columns.include?(name) || models.any? { |model| virtual_fields(model).include?(name) || self.class.declares?(model, name) }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def variant?(name)
|
|
120
|
+
bases = models.flat_map { |model| model.attribute_names + virtual_fields(model).names }
|
|
121
|
+
(columns + bases + siblings).uniq.any? do |base|
|
|
122
|
+
base != name && @configuration.field_variants_for(base).include?(name)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# The model and its loaded subclasses on the same table (STI).
|
|
127
|
+
def models
|
|
128
|
+
@models ||= [@model] + @model.descendants.select { |sub| sub.table_name == @model.table_name }
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def siblings
|
|
132
|
+
@siblings = Array(@siblings.respond_to?(:call) ? @siblings.call : @siblings).map(&:to_s)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# The table's columns as the database has them: ignored_columns too.
|
|
136
|
+
def columns
|
|
137
|
+
SchemaRead.columns(@model).keys
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def virtual_fields(model)
|
|
141
|
+
@virtual_fields ||= {}
|
|
142
|
+
@virtual_fields[model] ||= VirtualFields.new(model, @configuration)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
# The names a field row's setting also applies to on one Resource:
|
|
5
|
+
# Configuration#field_variants_for (config.field_variant_mappings:
|
|
6
|
+
# `owner_id` → `owner_uuid`, `price_cents` → `price`), kept to the names
|
|
7
|
+
# the Resource's model has —
|
|
8
|
+
#
|
|
9
|
+
# - an attribute (a column, or one declared with `attribute`),
|
|
10
|
+
# - an `alias_attribute`,
|
|
11
|
+
# - a public reader or writer (a `delegate :uuid, to: :owner, prefix: true`
|
|
12
|
+
# defines `owner_uuid`; `def owner_uuid=(uuid)` takes one), or
|
|
13
|
+
# - a virtual field, declared or discovered (PermissionForge::VirtualFields#include?)
|
|
14
|
+
#
|
|
15
|
+
# — the answer FieldNames gives (FieldNames.declares?, one definition). A
|
|
16
|
+
# name the table has no column for is asked again after the schema is
|
|
17
|
+
# read again, once (PermissionForge::SchemaRead): a variant column a
|
|
18
|
+
# migration just added gets its entry.
|
|
19
|
+
#
|
|
20
|
+
# A name the model does not have would be an entry in the permission hash
|
|
21
|
+
# for nothing (the admin's Effective access listed it as a field and
|
|
22
|
+
# counted it). A model that cannot be read — no such constant (a Resource
|
|
23
|
+
# shared with another app), not an Active Record model, a schema that
|
|
24
|
+
# cannot be loaded — keeps every variant, as before 0.15.
|
|
25
|
+
#
|
|
26
|
+
# A hash a serializer builds can still carry such a name (`owner_uuid`
|
|
27
|
+
# computed in a view): FiltersResponseAttributes reads it by the entry of
|
|
28
|
+
# the field it is a variant of (.base_entries), as when the hash had it.
|
|
29
|
+
#
|
|
30
|
+
# The one definition for the permission hash (granting and muting,
|
|
31
|
+
# PermissionHashBuilder / FieldPermissionMerger) and the admin
|
|
32
|
+
# (Admin::EffectiveFields, Admin::FieldDetail), which must agree.
|
|
33
|
+
#
|
|
34
|
+
# @example
|
|
35
|
+
# variants = PermissionForge::FieldVariants.new('Customer')
|
|
36
|
+
# variants.call('supplier_id') # => [] (Customer has no supplier_uuid)
|
|
37
|
+
#
|
|
38
|
+
# @api private
|
|
39
|
+
class FieldVariants
|
|
40
|
+
# Errors that mean the model's attributes cannot be read (no connection,
|
|
41
|
+
# a schema that cannot be loaded).
|
|
42
|
+
UNREADABLE = [ActiveRecord::ActiveRecordError].freeze
|
|
43
|
+
|
|
44
|
+
# The entries of the configured fields by the names of their variants
|
|
45
|
+
# (`owner_uuid` → the `owner_id` entry), to read a name that has no entry
|
|
46
|
+
# of its own. A name two fields map to (custom field_variant_mappings)
|
|
47
|
+
# gets the strictest of their entries (ResourceChain.strictest, the one
|
|
48
|
+
# definition): such a key is never more readable than any of its bases.
|
|
49
|
+
# This covers names the model does
|
|
50
|
+
# not have (serializer keys); a variant the model has gets its entry from
|
|
51
|
+
# FieldPermissionMerger, which merges every base row like any granting row.
|
|
52
|
+
#
|
|
53
|
+
# @param fields [Hash] a resource's field entries, by name
|
|
54
|
+
# @return [Hash{String => Hash}]
|
|
55
|
+
def self.base_entries(fields, configuration = PermissionForge.configuration)
|
|
56
|
+
bases = fields.each_with_object({}) do |(base, entry), by_variant|
|
|
57
|
+
configuration.field_variants_for(base).each { |variant| (by_variant[variant] ||= []) << entry }
|
|
58
|
+
end
|
|
59
|
+
bases.transform_values { |entries| ResourceChain.strictest(entries) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @param resource_name [String, Symbol] the Resource's name (a model's class name)
|
|
63
|
+
# @param configuration [Configuration]
|
|
64
|
+
def initialize(resource_name, configuration = PermissionForge.configuration)
|
|
65
|
+
@resource_name = resource_name.to_s
|
|
66
|
+
@configuration = configuration
|
|
67
|
+
@variants = {}
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# @param field_name [String, Symbol]
|
|
71
|
+
# @return [Array<String>] the variants of the field that the model has
|
|
72
|
+
def call(field_name)
|
|
73
|
+
@variants[field_name.to_s] ||= @configuration.field_variants_for(field_name).select { |name| attribute?(name) }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def attribute?(name)
|
|
79
|
+
return true unless model
|
|
80
|
+
|
|
81
|
+
FieldNames.declares?(model, name) || virtual_fields.include?(name)
|
|
82
|
+
rescue *UNREADABLE
|
|
83
|
+
true
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def model
|
|
87
|
+
return @model if defined?(@model)
|
|
88
|
+
|
|
89
|
+
@model = ModelLookup.model_for(@resource_name)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def virtual_fields
|
|
93
|
+
@virtual_fields ||= VirtualFields.new(model, @configuration)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
# The one definition of "this stored text is a default the gem generated",
|
|
5
|
+
# not something an admin wrote. Through 0.15 the gem stored English it had
|
|
6
|
+
# made up — a field or action row's label (`name.humanize`, the install
|
|
7
|
+
# migration's 'Untitled Field'), a rule's blocked message and the names of
|
|
8
|
+
# a set, a group or a rule created without one (the install migration's
|
|
9
|
+
# column defaults) — and showed it as if an admin had typed it, in every
|
|
10
|
+
# language. The writers store the name itself now and the models store
|
|
11
|
+
# nothing for a message or name nobody wrote; the rows stored before keep
|
|
12
|
+
# their text (no data is rewritten), and whatever displays one asks here
|
|
13
|
+
# and renders the locale's own words instead.
|
|
14
|
+
#
|
|
15
|
+
# Asked by PermissionRuleApplier.failure_message_for (a denial's
|
|
16
|
+
# message), the admin's labels (Admin::FieldLabel, Admin::ActionLabel: a
|
|
17
|
+
# generated label is no label), names (Admin::RecordName: an install
|
|
18
|
+
# default is named in the page's language) and form pre-fills
|
|
19
|
+
# (Admin::Prefill: blank for generated text).
|
|
20
|
+
#
|
|
21
|
+
# @example
|
|
22
|
+
# GeneratedText.label?('Owner', 'owner_id') # => true
|
|
23
|
+
# GeneratedText.failure_message?('You do not have permission to perform this action.') # => true
|
|
24
|
+
# GeneratedText.name?('Untitled Rule', :permission_set_rule) # => true
|
|
25
|
+
#
|
|
26
|
+
# @api private
|
|
27
|
+
module GeneratedText
|
|
28
|
+
# The label the install migration gives a field row (the column default).
|
|
29
|
+
UNTITLED_FIELD = 'Untitled Field'
|
|
30
|
+
# The names the install migration gave a set, a group and a rule created
|
|
31
|
+
# without one (the column defaults of installs made before 0.15), by kind
|
|
32
|
+
# (the model's `model_name.element`).
|
|
33
|
+
NAMES = {
|
|
34
|
+
permission_set: 'Untitled Permission Set',
|
|
35
|
+
permission_set_group: 'Untitled Permission Set Group',
|
|
36
|
+
permission_set_rule: 'Untitled Rule'
|
|
37
|
+
}.freeze
|
|
38
|
+
# The blocked messages the install migration gave a rule created without
|
|
39
|
+
# one (the column default of installs made before 0.15).
|
|
40
|
+
FAILURE_MESSAGES = ['You do not have permission to perform this action.'].freeze
|
|
41
|
+
|
|
42
|
+
module_function
|
|
43
|
+
|
|
44
|
+
# Whether a field or action row's label is nothing an admin wrote: blank,
|
|
45
|
+
# the name itself, the name humanized (what the gem stored through 0.15)
|
|
46
|
+
# or the install default. A label an admin typed that happens to equal
|
|
47
|
+
# the humanized name reads the same as one the gem generated — and as
|
|
48
|
+
# what Rails' own English would show for it.
|
|
49
|
+
#
|
|
50
|
+
# @param text [String, nil] the stored label
|
|
51
|
+
# @param name [String, Symbol] the row's name (a column, a virtual field, an action key)
|
|
52
|
+
# @return [Boolean]
|
|
53
|
+
def label?(text, name)
|
|
54
|
+
label = text.to_s.strip
|
|
55
|
+
return true if label.empty? || label == UNTITLED_FIELD
|
|
56
|
+
|
|
57
|
+
name = name.to_s
|
|
58
|
+
label == name || label == name.to_s.humanize
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Whether a rule's blocked message is the install migration's default.
|
|
62
|
+
#
|
|
63
|
+
# @param text [String, nil] the stored failure_message
|
|
64
|
+
# @return [Boolean]
|
|
65
|
+
def failure_message?(text)
|
|
66
|
+
FAILURE_MESSAGES.include?(text.to_s.strip)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Whether a set's, group's or rule's name is the install migration's
|
|
70
|
+
# default for its kind.
|
|
71
|
+
#
|
|
72
|
+
# @param text [String, nil] the stored name
|
|
73
|
+
# @param kind [Symbol, Class, ActiveRecord::Base] :permission_set,
|
|
74
|
+
# :permission_set_group or :permission_set_rule — or the model (class or record)
|
|
75
|
+
# @return [Boolean]
|
|
76
|
+
def name?(text, kind)
|
|
77
|
+
NAMES[kind_of(kind)] == text.to_s.strip
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Every text the install migration has given a column of the nine tables
|
|
81
|
+
# (an upgraded install still has them as column defaults): what no form
|
|
82
|
+
# may pre-fill.
|
|
83
|
+
#
|
|
84
|
+
# @return [Array<String>]
|
|
85
|
+
def install_defaults
|
|
86
|
+
[UNTITLED_FIELD, *NAMES.values, *FAILURE_MESSAGES]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def kind_of(kind)
|
|
90
|
+
kind = kind.class if kind.is_a?(ActiveRecord::Base)
|
|
91
|
+
kind.respond_to?(:model_name) ? kind.model_name.element.to_sym : kind.to_s.to_sym
|
|
92
|
+
end
|
|
93
|
+
private_class_method :kind_of
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
# What #inspect hides on the gem's own models. Rails copies
|
|
5
|
+
# config.filter_parameters into ActiveRecord::Base.filter_attributes, so
|
|
6
|
+
# the GDPR patterns PermissionForge adds there (filter_parameters_integration:
|
|
7
|
+
# `\Aname\z`, …) also hid the *configuration* names of the gem's rows: a
|
|
8
|
+
# console printed `#<PermissionForge::PermissionSet … name: [FILTERED]>` for a
|
|
9
|
+
# set called "Sales rep", and a ResourceField's `name` (a column name such as
|
|
10
|
+
# "email") the same way.
|
|
11
|
+
#
|
|
12
|
+
# Each model sets its own list with Rails' `filter_attributes=` as its
|
|
13
|
+
# class body runs (after config/initializers, again after a reload):
|
|
14
|
+
#
|
|
15
|
+
# - .show_configuration_names: the inherited filters, each made unable to
|
|
16
|
+
# match the model's configuration-name attributes (`name`, `label`,
|
|
17
|
+
# `field_name`) — they keep hiding every other attribute they matched (a
|
|
18
|
+
# rule's `field_value` or `failure_message`, a description). A Proc filter
|
|
19
|
+
# is kept as it is: it decides for itself.
|
|
20
|
+
# - .hide_people_labels (AuditEvent): the inherited filters plus the
|
|
21
|
+
# labels that name people (`actor_label`, `record_label`, which
|
|
22
|
+
# DataSubjectErasure scrubs), whenever the inherited filters hide a
|
|
23
|
+
# person's `name` — with the GDPR patterns, or a host's own `:name`.
|
|
24
|
+
#
|
|
25
|
+
# Only Regexps are added, so Rails 8's FilterAttributeHandler (which copies a
|
|
26
|
+
# model's String and Symbol entries back into config.filter_parameters as
|
|
27
|
+
# "model.attribute") adds nothing to the host's request-parameter filter.
|
|
28
|
+
# Request parameters, SQL bind logging (ActiveRecord::Base's filter) and the
|
|
29
|
+
# host's models are untouched. A filter added to
|
|
30
|
+
# ActiveRecord::Base.filter_attributes after a model has loaded does not
|
|
31
|
+
# reach it until the next reload.
|
|
32
|
+
#
|
|
33
|
+
# @api private
|
|
34
|
+
module InspectionFilters
|
|
35
|
+
# The attributes of the gem's models that hold configuration names.
|
|
36
|
+
CONFIGURATION_NAMES = %w[name label field_name].freeze
|
|
37
|
+
# AuditEvent's labels that name a person.
|
|
38
|
+
PEOPLE_LABELS = %w[actor_label record_label].freeze
|
|
39
|
+
|
|
40
|
+
module_function
|
|
41
|
+
|
|
42
|
+
# @param klass [Class] a model of the gem
|
|
43
|
+
# @return [void]
|
|
44
|
+
def show_configuration_names(klass, names = CONFIGURATION_NAMES)
|
|
45
|
+
klass.filter_attributes = sparing(klass.superclass.filter_attributes, names)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @param klass [Class] PermissionForge::AuditEvent
|
|
49
|
+
# @return [void]
|
|
50
|
+
def hide_people_labels(klass, labels = PEOPLE_LABELS)
|
|
51
|
+
inherited = klass.superclass.filter_attributes
|
|
52
|
+
return unless hides?(inherited, 'name')
|
|
53
|
+
|
|
54
|
+
# The inherited entries as Regexps too (sparing nothing): a String or
|
|
55
|
+
# Symbol here would be copied into config.filter_parameters.
|
|
56
|
+
klass.filter_attributes = sparing(inherited, []) + [/\A(?:#{labels.map { |label| Regexp.escape(label) }.join('|')})\z/]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# `filters` (a filter_attributes list) with every String, Symbol and
|
|
60
|
+
# Regexp turned into a Regexp that matches what it matched, except the
|
|
61
|
+
# attribute names `names` themselves (none when `names` is empty). A
|
|
62
|
+
# String or Symbol matches as Rails reads it: anywhere in the name,
|
|
63
|
+
# ignoring case.
|
|
64
|
+
#
|
|
65
|
+
# @return [Array]
|
|
66
|
+
def sparing(filters, names)
|
|
67
|
+
guard = names.empty? ? '' : "\\A(?!(?:#{names.map { |name| Regexp.escape(name.to_s) }.join('|')})\\z).*?"
|
|
68
|
+
filters.map do |filter|
|
|
69
|
+
case filter
|
|
70
|
+
when Proc then filter
|
|
71
|
+
when Regexp
|
|
72
|
+
# Under /x a trailing comment would swallow the closing parenthesis.
|
|
73
|
+
Regexp.new("#{guard}(?:#{filter.source}#{"\n" if filter.options.anybits?(Regexp::EXTENDED)})", filter.options)
|
|
74
|
+
else Regexp.new("#{guard}(?:#{Regexp.escape(filter.to_s)})", Regexp::IGNORECASE)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def hides?(filters, attribute)
|
|
80
|
+
ActiveSupport::ParameterFilter.new(filters).filter_param(attribute, 'x') != 'x'
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
# The one definition of the fields a model's Resource has rows for: its
|
|
5
|
+
# columns (`ignored_columns` and config.ignored_fields left out), then its
|
|
6
|
+
# virtual fields (PermissionForge::VirtualFields: declared and
|
|
7
|
+
# discovered), whatever extra_fields_default_access says. Every path that
|
|
8
|
+
# creates rows for a model writes rows for these names and no others —
|
|
9
|
+
# the admin's presets, "Sync fields" and bulk add (a posted name outside
|
|
10
|
+
# them is no row), and ResourceFieldGenerator — so a level chosen for "all
|
|
11
|
+
# fields" (Hidden, say) reaches a virtual field on every path. The admin's
|
|
12
|
+
# model catalog lists them (Admin::ModelCatalog::ModelInfo#managed_field_names).
|
|
13
|
+
#
|
|
14
|
+
# The columns are read through PermissionForge::SchemaRead: compared with
|
|
15
|
+
# the database first with `verify: true` (a list that builds rows), from
|
|
16
|
+
# the schema cache without it (a count on every admin page).
|
|
17
|
+
#
|
|
18
|
+
# @api private
|
|
19
|
+
class ManagedFields
|
|
20
|
+
# @param model [Class] an Active Record model with a table
|
|
21
|
+
# @param configuration [Configuration]
|
|
22
|
+
# @param verify [Boolean] compare the columns with the database first
|
|
23
|
+
def initialize(model, configuration: PermissionForge.configuration, verify: true)
|
|
24
|
+
@model = model
|
|
25
|
+
@configuration = configuration
|
|
26
|
+
@verify = verify
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @return [Array<String>] the model's columns (`ignored_columns` excluded)
|
|
30
|
+
def column_names
|
|
31
|
+
@column_names ||= SchemaRead.column_names(@model, verify: @verify)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @return [Array<String>] the columns config.ignored_fields leaves out
|
|
35
|
+
def ignored_column_names
|
|
36
|
+
column_names & ignored_fields
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @return [Array<String>] the columns that get rows
|
|
40
|
+
def managed_column_names
|
|
41
|
+
column_names - ignored_fields
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @return [Array<String>] declared and discovered virtual fields
|
|
45
|
+
def virtual_field_names
|
|
46
|
+
virtual_fields.names
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @return [Array<String>] managed columns, then virtual fields
|
|
50
|
+
def names
|
|
51
|
+
managed_column_names + virtual_field_names
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @return [VirtualFields]
|
|
55
|
+
def virtual_fields
|
|
56
|
+
@virtual_fields ||= VirtualFields.new(@model, @configuration)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
# Symbols or Strings, as the host wrote them.
|
|
62
|
+
def ignored_fields
|
|
63
|
+
@ignored_fields ||= Array(@configuration.ignored_fields).map(&:to_s)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'database_reach'
|
|
4
|
+
|
|
3
5
|
module PermissionForge
|
|
4
6
|
# Auto-applied masking: mixes MaskedAttributes into the models
|
|
5
7
|
# config.auto_apply_masked_attributes selects and wraps the virtual readers
|
|
@@ -44,11 +46,12 @@ module PermissionForge
|
|
|
44
46
|
def apply(only: nil)
|
|
45
47
|
config = PermissionForge.configuration
|
|
46
48
|
return true unless enabled?(config)
|
|
47
|
-
|
|
49
|
+
# No database to use (not configured, unreachable), or no table yet.
|
|
50
|
+
return false unless DatabaseReach.table_exists?(PermissionForge::Resource)
|
|
48
51
|
|
|
49
52
|
apply_selected(config, only).all? & unknown_listed_names(config, only).empty?
|
|
50
|
-
rescue
|
|
51
|
-
#
|
|
53
|
+
rescue *DatabaseReach::READ_ERRORS
|
|
54
|
+
# The database went away while the rows were read.
|
|
52
55
|
false
|
|
53
56
|
end
|
|
54
57
|
|
|
@@ -162,8 +165,9 @@ module PermissionForge
|
|
|
162
165
|
def warn_unknown(setting, name)
|
|
163
166
|
return if unmaskable_warned.put_if_absent("#{setting}=#{name}", true)
|
|
164
167
|
|
|
165
|
-
|
|
166
|
-
|
|
168
|
+
Warnings.emit("masked_models.unknown_listed.#{setting}=#{name}",
|
|
169
|
+
"'#{name}' is listed in config.#{setting} but is no model in this process: nothing is " \
|
|
170
|
+
'masked for it. It is tried again at the next request or job.', severity: :security)
|
|
167
171
|
end
|
|
168
172
|
|
|
169
173
|
# Every tenant's rows: a model is masked for the whole process.
|
|
@@ -187,29 +191,31 @@ module PermissionForge
|
|
|
187
191
|
#
|
|
188
192
|
# @return [String, nil] why the concern cannot be included now
|
|
189
193
|
def unmaskable_reason(klass)
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
'an earlier attempt failed halfway; restart the process' if klass.include?(PermissionForge::Concerns::MaskedAttributes)
|
|
194
|
+
unreadable_table(klass) ||
|
|
195
|
+
('an earlier attempt failed halfway; restart the process' if klass.include?(PermissionForge::Concerns::MaskedAttributes))
|
|
193
196
|
end
|
|
194
197
|
|
|
195
|
-
#
|
|
196
|
-
# again next time (the cached answer is
|
|
197
|
-
|
|
198
|
-
|
|
198
|
+
# Why the table cannot be read, nil when it exists and can be. A table
|
|
199
|
+
# found missing is looked up again next time (the cached answer is
|
|
200
|
+
# dropped): a migration may run. Its database may be another one than
|
|
201
|
+
# the gem's (a host on several databases); a check that raised names
|
|
202
|
+
# the error (DatabaseReach): the model's own table_name, its database
|
|
203
|
+
# not configured.
|
|
204
|
+
def unreadable_table(klass)
|
|
205
|
+
cause = nil
|
|
206
|
+
return if DatabaseReach.table_exists?(klass) { |error| cause = error }
|
|
199
207
|
|
|
200
|
-
|
|
201
|
-
false
|
|
202
|
-
rescue ActiveRecord::StatementInvalid, ActiveRecord::ConnectionNotEstablished
|
|
203
|
-
false
|
|
208
|
+
cause ? "its table cannot be read (#{cause.class}: #{cause.message.lines.first&.strip})" : 'its table cannot be read'
|
|
204
209
|
end
|
|
205
210
|
|
|
206
|
-
#
|
|
211
|
+
# Warns once per model and process (MaskingSync retries every request).
|
|
207
212
|
def warn_unmaskable(klass, why)
|
|
208
213
|
name = klass.to_s
|
|
209
214
|
return if unmaskable_warned.put_if_absent(name, true)
|
|
210
215
|
|
|
211
|
-
|
|
212
|
-
|
|
216
|
+
Warnings.emit("masked_models.unmaskable.#{name}",
|
|
217
|
+
"#{name} has a Resource row but #{why}: its columns are NOT masked in this process. " \
|
|
218
|
+
'It is tried again at the next request or job.', severity: :security)
|
|
213
219
|
end
|
|
214
220
|
|
|
215
221
|
# The models (and unknown listed names) whose warning this process has logged.
|