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
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'active_support/core_ext/module/delegation'
|
|
4
|
+
|
|
3
5
|
module PermissionForge
|
|
4
6
|
module Operators
|
|
5
7
|
# The model column a rule's field_name names, as far as it can be told:
|
|
@@ -16,58 +18,212 @@ module PermissionForge
|
|
|
16
18
|
# text after all. A lookup that is more generous than the database costs
|
|
17
19
|
# nothing: the database then rejects the name itself.
|
|
18
20
|
#
|
|
19
|
-
# When the table is none of the rule's,
|
|
20
|
-
#
|
|
21
|
-
#
|
|
21
|
+
# When the table is none of the rule's, what the name is depends on the
|
|
22
|
+
# names the rule's query gives its tables ({RuleSources}: the FROM and
|
|
23
|
+
# JOINs, with the aliases ActiveRecord assigns — never a name that merely
|
|
24
|
+
# appears in a join condition): a name the join gives that is no model
|
|
25
|
+
# table (`parents_customers` on a self-join) is {UNKNOWN} and left to the
|
|
26
|
+
# database — unless the column it names holds ciphertext
|
|
27
|
+
# ({EncryptedAlias}: the name is resolved to the table it stands for and
|
|
28
|
+
# every loaded model on that table — a `has_many :through`'s join table
|
|
29
|
+
# and a table a default scope joins too; one that cannot be resolved
|
|
30
|
+
# counts as encrypted when any model along the path encrypts a column of
|
|
31
|
+
# that name), when it is that model's column, {#encrypted?}; a real table
|
|
32
|
+
# that is none of the rule's is {FOREIGN} (a filter
|
|
33
|
+
# rule compares its model's own table: it matches nothing; a join rule's
|
|
34
|
+
# query has to join that table itself); anything else is {#unresolvable?}
|
|
35
|
+
# ({UNRESOLVABLE}) and the rule matches nothing — the database would
|
|
36
|
+
# reject the query (`no such column: supplier_id.id`), and every holder
|
|
37
|
+
# would get an error instead of a denial. A table of the rule's that has
|
|
38
|
+
# no such column is {#missing?}: the database would reject the query, so
|
|
39
|
+
# a built-in operator matches nothing for it (ValueTyping.no_column?). A
|
|
40
|
+
# field_name that is not a name ({.name?}) is {#malformed?}: such a rule
|
|
41
|
+
# matches nothing. A column of an attribute declared with `encrypts`
|
|
42
|
+
# holds ciphertext ({#encrypted?}): no built-in operator that takes a
|
|
43
|
+
# value can compare it (ValueTyping).
|
|
22
44
|
class RuleColumn
|
|
23
45
|
# Adapters whose database compares a quoted name as it is written. On
|
|
24
46
|
# every other one — MySQL, SQLite — `Flag` is the column `flag`.
|
|
25
47
|
EXACT_NAMES = %w[PostgreSQL PostGIS].freeze
|
|
26
48
|
|
|
49
|
+
# What makes a rule's column or path "cannot tell" for a caller that
|
|
50
|
+
# only describes it (for_rule, RuleProblem.of_path — the admin, the
|
|
51
|
+
# audit, the model's checks): anything raised while the models are
|
|
52
|
+
# loaded or asked, a model file's SyntaxError or LoadError included.
|
|
53
|
+
# The applier's own walk does not rescue them (RulePath).
|
|
54
|
+
UNTELLABLE = [StandardError, ScriptError].freeze
|
|
55
|
+
# A character no name has (see .name?).
|
|
56
|
+
CONTROL_CHARACTER = /[[:cntrl:]]/
|
|
57
|
+
|
|
27
58
|
# @return [ActiveRecord::ConnectionAdapters::Column, nil]
|
|
28
59
|
attr_reader :column
|
|
29
60
|
# @return [Hash, nil] label => stored value, when the model declares the column an enum
|
|
30
61
|
attr_reader :enum
|
|
62
|
+
# @return [Class, nil] the model whose table the name was looked up in; nil when no table was
|
|
63
|
+
attr_reader :model
|
|
31
64
|
|
|
32
65
|
class << self
|
|
33
66
|
# A field name is one name, or several joined by dots. An empty one
|
|
34
67
|
# among them — a leading, trailing or doubled dot, or nothing at all —
|
|
35
68
|
# makes it no name: quoted segment by segment it would be another
|
|
36
|
-
# name than the one that was written.
|
|
69
|
+
# name than the one that was written. So does a control character (a
|
|
70
|
+
# newline, a NUL byte) in one: no column is called that, and quoted
|
|
71
|
+
# into a statement it reaches the database as written.
|
|
37
72
|
def name?(field_name)
|
|
38
73
|
segments = field_name.to_s.split('.', -1)
|
|
39
|
-
segments.any? && segments.none?
|
|
74
|
+
segments.any? && segments.none? { |segment| segment.empty? || segment.match?(CONTROL_CHARACTER) }
|
|
40
75
|
end
|
|
41
76
|
|
|
42
77
|
# @param field_name [String] the rule's field_name
|
|
43
78
|
# @param models [Array<Class>] the rule's model, then the model of each
|
|
44
79
|
# association on its path (see {.models_along})
|
|
80
|
+
# @param joined [Hash{String => String, nil}, Array<String>, nil] the
|
|
81
|
+
# names a join rule's query gives its tables, with the table each
|
|
82
|
+
# stands for ({RuleSources.tables}; a list of names: tables not
|
|
83
|
+
# known); nil for a filter rule, whose query joins nothing of its own
|
|
45
84
|
# @return [RuleColumn]
|
|
46
|
-
def find(field_name, models)
|
|
85
|
+
def find(field_name, models, joined: nil)
|
|
47
86
|
return MALFORMED unless name?(field_name)
|
|
87
|
+
return UNKNOWN if models.empty?
|
|
48
88
|
|
|
49
89
|
*qualifier, name = field_name.to_s.split('.')
|
|
50
|
-
|
|
51
|
-
model
|
|
90
|
+
table = qualifier.last
|
|
91
|
+
model = table_model(table, models) || EncryptedAlias.new(models, joined).model(table, name)
|
|
92
|
+
model ? on(model, name) : elsewhere(table, models.last, joined)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Whether the connection has a data source (a table or view) named
|
|
96
|
+
# `table`: asked only for a qualifier that is none of the rule's
|
|
97
|
+
# tables. A "no" is asked of the database again once
|
|
98
|
+
# (PermissionForge::SchemaRead.table?): the schema cache may predate
|
|
99
|
+
# a table a migration just created.
|
|
100
|
+
#
|
|
101
|
+
# @param model [Class] a model on the connection
|
|
102
|
+
# @param table [String] the name as written
|
|
103
|
+
def data_source?(model, table)
|
|
104
|
+
PermissionForge::SchemaRead.table?(model, table)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# The columns a rule may compare on a model: every column of its
|
|
108
|
+
# table as the database knows them (a model's `ignored_columns`
|
|
109
|
+
# among them, as in .find) — the names the admin's rule builder
|
|
110
|
+
# offers and the model's validation accepts, one list.
|
|
111
|
+
#
|
|
112
|
+
# The list is compared with the database first
|
|
113
|
+
# (PermissionForge::SchemaRead, `verify: true`): a column a migration
|
|
114
|
+
# added since the schema was read is offered.
|
|
115
|
+
#
|
|
116
|
+
# @param model [Class] a model with a table
|
|
117
|
+
# @param verify [Boolean] false when the caller compared the columns already
|
|
118
|
+
# @return [Array<String>]
|
|
119
|
+
def names_for(model, verify: true)
|
|
120
|
+
PermissionForge::SchemaRead.columns(model, verify: verify).keys
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# The column a rule row names, looked up the way PermissionRuleApplier
|
|
124
|
+
# looks it up: in the model its Resource is named after and along its
|
|
125
|
+
# association path (join rules only). nil when that cannot be told —
|
|
126
|
+
# the Resource names no model, the model cannot be loaded (its file
|
|
127
|
+
# raises anything as it loads: UNTELLABLE), the path cannot be
|
|
128
|
+
# followed, a table along it does not exist (asking a missing table
|
|
129
|
+
# for its columns fails the statement, and with it a transaction) or
|
|
130
|
+
# the database cannot be asked.
|
|
131
|
+
#
|
|
132
|
+
# `verify: true` (a rule being saved) compares the columns of every
|
|
133
|
+
# model along the path with the database first (one metadata query
|
|
134
|
+
# each, PermissionForge::SchemaRead): a name the process confirmed
|
|
135
|
+
# missing before a migration added it is accepted at save. The
|
|
136
|
+
# describing callers (the admin's pages, the audit) ask without it.
|
|
137
|
+
#
|
|
138
|
+
# @param rule [PermissionForge::PermissionSetRule]
|
|
139
|
+
# @param verify [Boolean]
|
|
140
|
+
# @return [RuleColumn, nil]
|
|
141
|
+
def for_rule(rule, verify: false)
|
|
142
|
+
model = rule_model(rule)
|
|
143
|
+
return unless model
|
|
144
|
+
|
|
145
|
+
chain = rule.join_rule? ? path_segments(rule.field_accessor_path) : []
|
|
146
|
+
models = models_along(model, chain)
|
|
147
|
+
find(rule.field_name, models, joined: rule_join_names(rule, model, chain)) if readable?(models, verify)
|
|
148
|
+
rescue *UNTELLABLE
|
|
149
|
+
nil
|
|
52
150
|
end
|
|
53
151
|
|
|
54
152
|
# The rule's own model, then the model of each association on its path;
|
|
55
|
-
# a plain field_name is a column of the last one.
|
|
153
|
+
# a plain field_name is a column of the last one. The path is
|
|
154
|
+
# followed by {RulePath}, the way ActiveRecord joins it.
|
|
56
155
|
#
|
|
57
156
|
# @param model [Class] the rule's model
|
|
58
157
|
# @param association_chain [Array<String>] the rule's field_accessor_path, split
|
|
59
|
-
# @return [Array<Class>] empty when the path cannot be followed
|
|
158
|
+
# @return [Array<Class>] empty when the path cannot be followed (RulePath#broken?)
|
|
60
159
|
def models_along(model, association_chain)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
160
|
+
RulePath.follow(model, association_chain).models
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# A rule's association path (its field_accessor_path), split into
|
|
164
|
+
# the association names the applier joins; a blank one is no path.
|
|
165
|
+
#
|
|
166
|
+
# @param path [String, nil]
|
|
167
|
+
# @return [Array<String>]
|
|
168
|
+
def path_segments(path)
|
|
169
|
+
path.blank? ? [] : path.to_s.split('.')
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# The model the rule's Resource is named after, when it is one with a
|
|
173
|
+
# table (PermissionForge::ModelLookup, the one definition).
|
|
174
|
+
#
|
|
175
|
+
# @param rule [PermissionForge::PermissionSetRule]
|
|
176
|
+
# @return [Class, nil]
|
|
177
|
+
def rule_model(rule)
|
|
178
|
+
PermissionForge::ModelLookup.model_for(rule.resource&.name)
|
|
179
|
+
end
|
|
64
180
|
|
|
65
|
-
|
|
66
|
-
|
|
181
|
+
# Whether `written` names `own` the way the database compares names
|
|
182
|
+
# (exactly where it compares a quoted name as written: EXACT_NAMES).
|
|
183
|
+
#
|
|
184
|
+
# @param own [String] the name as the schema or the model has it
|
|
185
|
+
# @param written [String] the name as the rule writes it
|
|
186
|
+
# @param exact [Boolean] see #exact_names?
|
|
187
|
+
def same_name?(own, written, exact)
|
|
188
|
+
own == written || (!exact && own.casecmp?(written))
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Whether the model's database compares a quoted name as written.
|
|
192
|
+
#
|
|
193
|
+
# @param model [Class]
|
|
194
|
+
def exact_names?(model)
|
|
195
|
+
EXACT_NAMES.include?(adapter_name(model))
|
|
67
196
|
end
|
|
68
197
|
|
|
69
198
|
private
|
|
70
199
|
|
|
200
|
+
# A qualifier that is none of the rule's models: a name the query
|
|
201
|
+
# gives a table (an alias: `joined` is a Hash of name => table, or a
|
|
202
|
+
# list of names), a real table, or nothing at all.
|
|
203
|
+
def elsewhere(table, model, joined)
|
|
204
|
+
return UNKNOWN if joined&.any? { |source, _table| same_name?(source, table, exact_names?(model)) }
|
|
205
|
+
|
|
206
|
+
data_source?(model, table) ? FOREIGN : UNRESOLVABLE
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# nil for a filter rule (no join of its own); a join rule's join
|
|
210
|
+
# names, read only for a qualified field_name.
|
|
211
|
+
def rule_join_names(rule, model, chain)
|
|
212
|
+
return unless rule.join_rule?
|
|
213
|
+
|
|
214
|
+
rule.field_name.to_s.include?('.') ? RuleSources.of_join(model, rule.join_type, chain) : {}
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Every table along the path is there (asking a missing one for its
|
|
218
|
+
# columns fails the statement); compared with the database first when
|
|
219
|
+
# `verify`.
|
|
220
|
+
def readable?(models, verify)
|
|
221
|
+
return false unless models.any? && models.all? { |model| PermissionForge::SchemaRead.table?(model, model.table_name) }
|
|
222
|
+
|
|
223
|
+
models.each { |model| PermissionForge::SchemaRead.columns(model, verify: true) } if verify
|
|
224
|
+
true
|
|
225
|
+
end
|
|
226
|
+
|
|
71
227
|
# The model a table name stands for; without one, the model the rule ends on.
|
|
72
228
|
def table_model(table, models)
|
|
73
229
|
return models.last unless table
|
|
@@ -75,45 +231,104 @@ module PermissionForge
|
|
|
75
231
|
models.find { |model| same_name?(model.table_name.to_s.split('.').last, table, exact_names?(model)) }
|
|
76
232
|
end
|
|
77
233
|
|
|
234
|
+
# The table's column of that name; a name the schema cache does not
|
|
235
|
+
# have is looked up again once after the schema is read again
|
|
236
|
+
# (PermissionForge::SchemaRead): a column a migration just added.
|
|
78
237
|
def on(model, name)
|
|
79
|
-
|
|
80
|
-
column =
|
|
81
|
-
new(column, column && model.defined_enums[column.name],
|
|
238
|
+
exact = exact_names?(model)
|
|
239
|
+
column = PermissionForge::SchemaRead.column(model, name) { |columns| column_named(columns, name, exact) }
|
|
240
|
+
new(column, column && model.defined_enums[column.name], adapter_name(model), model)
|
|
82
241
|
end
|
|
83
242
|
|
|
84
243
|
def column_named(columns, name, exact)
|
|
85
244
|
columns[name] || columns.each_value.find { |column| same_name?(column.name, name, exact) }
|
|
86
245
|
end
|
|
87
246
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def exact_names?(model)
|
|
93
|
-
EXACT_NAMES.include?(model.connection.adapter_name)
|
|
247
|
+
# Through the pool, never `model.connection` (it raises under
|
|
248
|
+
# ActiveRecord.permanent_connection_checkout = :disallowed).
|
|
249
|
+
def adapter_name(model)
|
|
250
|
+
model.connection_pool.with_connection(&:adapter_name)
|
|
94
251
|
end
|
|
95
252
|
end
|
|
96
253
|
|
|
97
|
-
def initialize(column, enum, adapter_name)
|
|
254
|
+
def initialize(column, enum, adapter_name, model = nil)
|
|
98
255
|
@column = column
|
|
99
256
|
@enum = enum
|
|
100
257
|
@adapter_name = adapter_name
|
|
258
|
+
@model = model
|
|
101
259
|
end
|
|
102
260
|
|
|
103
|
-
# Nothing is known about the column:
|
|
261
|
+
# Nothing is known about the column: the name's table is none of the
|
|
262
|
+
# rule's models but a name the rule's query gives a table (an alias a
|
|
263
|
+
# join gives), or the path cannot be followed.
|
|
104
264
|
UNKNOWN = new(nil, nil, nil).freeze
|
|
265
|
+
# The name's table is a real table that is none of the rule's (not its
|
|
266
|
+
# model's, not one along its path or its query's joins): a filter rule
|
|
267
|
+
# matches nothing (RuleProblem :foreign_table); a join rule's query has
|
|
268
|
+
# to join it itself, or it matches nothing too.
|
|
269
|
+
FOREIGN = new(nil, nil, nil).freeze
|
|
105
270
|
# The rule's field_name is not a name.
|
|
106
271
|
MALFORMED = new(nil, nil, nil).freeze
|
|
272
|
+
# The name's table is none of the rule's and no table of the database
|
|
273
|
+
# (a rule that joins nothing): the database would reject the query.
|
|
274
|
+
UNRESOLVABLE = new(nil, nil, nil).freeze
|
|
107
275
|
|
|
108
276
|
def malformed?
|
|
109
277
|
equal?(MALFORMED)
|
|
110
278
|
end
|
|
111
279
|
|
|
280
|
+
# The name is qualified with a real table that is none of the rule's (see FOREIGN).
|
|
281
|
+
def foreign?
|
|
282
|
+
equal?(FOREIGN)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# The name is qualified with a table that is not there (see UNRESOLVABLE):
|
|
286
|
+
# such a rule matches nothing.
|
|
287
|
+
def unresolvable?
|
|
288
|
+
equal?(UNRESOLVABLE)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# The name's table is one of the rule's ({#model}), and it has no such
|
|
292
|
+
# column: the database would reject a query that compares it.
|
|
293
|
+
def missing?
|
|
294
|
+
!model.nil? && column.nil?
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Whether the column holds ciphertext: an attribute of {#model} — the
|
|
298
|
+
# class the rule is applied to (the applier's model class), one along
|
|
299
|
+
# its path, or one on a table its query joins (EncryptedAlias) —
|
|
300
|
+
# declared with `encrypts` by it or a class of its hierarchy on its
|
|
301
|
+
# table (PermissionForge::EncryptedAttributes). Asked with the column's
|
|
302
|
+
# own name (a field name written in another case resolves to it).
|
|
303
|
+
def encrypted?
|
|
304
|
+
return false if model.nil? || column.nil?
|
|
305
|
+
|
|
306
|
+
PermissionForge::EncryptedAttributes.encrypted?(model, column.name)
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
class << self
|
|
310
|
+
# @!method encrypted?(model, name)
|
|
311
|
+
# Whether `name` is a column of `model` that holds ciphertext (an
|
|
312
|
+
# attribute declared with `encrypts`; PermissionForge::EncryptedAttributes,
|
|
313
|
+
# the one definition): a rule can only ask whether it is empty. What
|
|
314
|
+
# the admin's rule builder asks before it offers a column.
|
|
315
|
+
# @param model [Class, nil] the model whose table the column is in
|
|
316
|
+
# @param name [String, Symbol] the column's name, as the model has it
|
|
317
|
+
# @return [Boolean]
|
|
318
|
+
delegate :encrypted?, to: PermissionForge::EncryptedAttributes
|
|
319
|
+
end
|
|
320
|
+
|
|
112
321
|
# The caster for the column's type, on the database it is in.
|
|
113
322
|
#
|
|
114
323
|
# @return [#call] see ValueCasters.for
|
|
115
324
|
def caster
|
|
116
|
-
ValueCasters.for(column, @adapter_name)
|
|
325
|
+
ValueCasters.for(column, @adapter_name, model)
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# Whether a LIKE pattern can be matched against the column, on the
|
|
329
|
+
# database it is in (ValueCasters.pattern?).
|
|
330
|
+
def pattern?
|
|
331
|
+
ValueCasters.pattern?(caster, @adapter_name)
|
|
117
332
|
end
|
|
118
333
|
end
|
|
119
334
|
end
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Operators
|
|
5
|
+
# A join rule's association path (its field_accessor_path, split on
|
|
6
|
+
# dots), followed from the rule's model the way ActiveRecord follows it
|
|
7
|
+
# when it builds the join: the one walk of a path (the applier, the
|
|
8
|
+
# column lookup, the model's validation and the audit all go through it).
|
|
9
|
+
#
|
|
10
|
+
# ActiveRecord raises for a join it cannot build — when the query runs,
|
|
11
|
+
# so in every find_by / permission_scoped of the rule's holders. Before
|
|
12
|
+
# it joins a name (Associations::JoinDependency#build) it looks the
|
|
13
|
+
# association up by that name and asks the reflection check_validity!
|
|
14
|
+
# and check_eager_loadable!; a polymorphic one raises, and the class of
|
|
15
|
+
# every other one is loaded. A segment that fails any of those is where
|
|
16
|
+
# the path {#broken?}:
|
|
17
|
+
#
|
|
18
|
+
# - :unknown_association — the model has no association of that name
|
|
19
|
+
# (a typo: `ownr`), or the segment is empty (`parent..owner`; cause
|
|
20
|
+
# :empty_segment);
|
|
21
|
+
# - :unjoinable_association — the association is there, but no join can
|
|
22
|
+
# be built through it; {#cause} says why (UNJOINABLE_CAUSES).
|
|
23
|
+
#
|
|
24
|
+
# Everything ActiveRecord joins — has_many / has_one / belongs_to,
|
|
25
|
+
# :through, has_and_belongs_to_many, a scoped association — is followed.
|
|
26
|
+
# A rule whose path is broken matches nothing (PermissionRuleApplier):
|
|
27
|
+
# the join would raise.
|
|
28
|
+
#
|
|
29
|
+
# A model file that raises a ScriptError (SyntaxError, LoadError) while
|
|
30
|
+
# it loads is not a broken path: the error propagates, from the applier
|
|
31
|
+
# too (the query raises; it never answers). An admin-facing caller that
|
|
32
|
+
# only describes the path (RuleProblem.of_path, RuleColumn.for_rule)
|
|
33
|
+
# treats it as "cannot tell".
|
|
34
|
+
#
|
|
35
|
+
# @example
|
|
36
|
+
# path = RulePath.follow(Customer, %w[supplier owner])
|
|
37
|
+
# path.models # => [Customer, Supplier, User]
|
|
38
|
+
# RulePath.follow(Customer, %w[ownr]).reason # => :unknown_association
|
|
39
|
+
class RulePath
|
|
40
|
+
# Why an association is there but cannot be joined (#cause of an
|
|
41
|
+
# :unjoinable_association path), in the order they are checked:
|
|
42
|
+
#
|
|
43
|
+
# - :invalid_definition — its :through, :source or inverse_of names an
|
|
44
|
+
# association that is not there (Reflection#check_validity! raises);
|
|
45
|
+
# - :polymorphic — a polymorphic belongs_to: no one class to join;
|
|
46
|
+
# - :record_dependent_scope — its scope, or one along its :through
|
|
47
|
+
# (the through association's or the source's), takes the record
|
|
48
|
+
# (`->(record) { … }`): a join has no record to give it;
|
|
49
|
+
# - :class_not_loadable — its class (or one along its :through) cannot
|
|
50
|
+
# be loaded: no such constant, or its file raised while it loaded.
|
|
51
|
+
UNJOINABLE_CAUSES = %i[invalid_definition polymorphic record_dependent_scope class_not_loadable].freeze
|
|
52
|
+
|
|
53
|
+
# @return [Array<Class>] the rule's model, then the model of each
|
|
54
|
+
# association on the path; empty when the path is broken
|
|
55
|
+
attr_reader :models
|
|
56
|
+
# @return [String, nil] the segment the path breaks at
|
|
57
|
+
attr_reader :segment
|
|
58
|
+
# @return [Class, nil] the model that segment was looked up in
|
|
59
|
+
attr_reader :owner
|
|
60
|
+
# @return [Symbol, nil] :unknown_association or :unjoinable_association
|
|
61
|
+
attr_reader :reason
|
|
62
|
+
# @return [Symbol, nil] :empty_segment for an empty segment, one of
|
|
63
|
+
# UNJOINABLE_CAUSES for an :unjoinable_association, else nil
|
|
64
|
+
attr_reader :cause
|
|
65
|
+
|
|
66
|
+
# @param model [Class] the rule's model
|
|
67
|
+
# @param association_chain [Array<String>] the rule's field_accessor_path, split
|
|
68
|
+
# @return [RulePath]
|
|
69
|
+
def self.follow(model, association_chain)
|
|
70
|
+
association_chain.reduce(new([model])) do |path, name|
|
|
71
|
+
path.through(name.to_s)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def initialize(models, segment: nil, owner: nil, reason: nil, cause: nil)
|
|
76
|
+
@models = models
|
|
77
|
+
@segment = segment
|
|
78
|
+
@owner = owner
|
|
79
|
+
@reason = reason
|
|
80
|
+
@cause = cause
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def broken?
|
|
84
|
+
!@reason.nil?
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# The path one association further; a broken path stays broken where it broke.
|
|
88
|
+
#
|
|
89
|
+
# @param name [String] the next segment
|
|
90
|
+
# @return [RulePath]
|
|
91
|
+
def through(name)
|
|
92
|
+
return self if broken?
|
|
93
|
+
|
|
94
|
+
owner = models.last
|
|
95
|
+
at = { segment: name, owner: owner }
|
|
96
|
+
return RulePath.new([], **at, reason: :unknown_association, cause: :empty_segment) if name.empty?
|
|
97
|
+
|
|
98
|
+
reflection = owner._reflect_on_association(name)
|
|
99
|
+
return RulePath.new([], **at, reason: :unknown_association) unless reflection
|
|
100
|
+
|
|
101
|
+
cause = RulePath.unjoinable_cause(reflection)
|
|
102
|
+
return RulePath.new([], **at, reason: :unjoinable_association, cause: cause) if cause
|
|
103
|
+
|
|
104
|
+
RulePath.new(models + [reflection.klass])
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
class << self
|
|
108
|
+
# The relation with the path joined, the way every rule's join is
|
|
109
|
+
# made (the applier's query, RuleColumn.join_names): one association
|
|
110
|
+
# by name, a longer path as the nested Hash ActiveRecord joins.
|
|
111
|
+
#
|
|
112
|
+
# @param relation [ActiveRecord::Relation]
|
|
113
|
+
# @param join_method [Symbol] one of PermissionRuleApplier::JOIN_METHODS — never a rule's own text
|
|
114
|
+
# @param association_chain [Array<String>] the rule's path, split
|
|
115
|
+
# @return [ActiveRecord::Relation]
|
|
116
|
+
def join(relation, join_method, association_chain)
|
|
117
|
+
return relation if association_chain.empty?
|
|
118
|
+
|
|
119
|
+
relation.public_send(join_method, join_structure(association_chain))
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Where ActiveRecord would raise building a join through the
|
|
123
|
+
# reflection, why (UNJOINABLE_CAUSES); nil when it builds one. A raise
|
|
124
|
+
# of any kind counts: it would be raised in the holders' queries.
|
|
125
|
+
# check_validity! and check_eager_loadable! are what JoinDependency
|
|
126
|
+
# asks of the reflection itself; a :through also evaluates the scopes
|
|
127
|
+
# along its chain (record_dependent?).
|
|
128
|
+
#
|
|
129
|
+
# @param reflection [ActiveRecord::Reflection::AbstractReflection]
|
|
130
|
+
# @return [Symbol, nil] one of UNJOINABLE_CAUSES
|
|
131
|
+
def unjoinable_cause(reflection)
|
|
132
|
+
failure(:invalid_definition) { reflection.check_validity! } ||
|
|
133
|
+
(:polymorphic if reflection.polymorphic?) ||
|
|
134
|
+
failure(:record_dependent_scope) { reflection.check_eager_loadable! } ||
|
|
135
|
+
failure(:class_not_loadable) { reflection.klass } ||
|
|
136
|
+
(:record_dependent_scope if record_dependent?(reflection))
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
private
|
|
140
|
+
|
|
141
|
+
# `supplier` => :supplier; `supplier.owner` => { supplier: :owner }.
|
|
142
|
+
def join_structure(association_chain)
|
|
143
|
+
*outer, last = association_chain.map(&:to_sym)
|
|
144
|
+
outer.reverse.reduce(last) { |inner, name| { name => inner } }
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# nil when the block raises nothing (what it answers is not looked
|
|
148
|
+
# at). When it raises: `cause` for what a definition or a check raises
|
|
149
|
+
# (an ActiveRecordError, ArgumentError or NoMethodError), else
|
|
150
|
+
# :class_not_loadable — a constant that is not there (NameError), or
|
|
151
|
+
# whatever a model file raised as it loaded.
|
|
152
|
+
def failure(cause)
|
|
153
|
+
yield
|
|
154
|
+
nil
|
|
155
|
+
rescue NoMethodError, ArgumentError, ActiveRecord::ActiveRecordError
|
|
156
|
+
cause
|
|
157
|
+
rescue StandardError
|
|
158
|
+
:class_not_loadable
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Whether a scope along the reflection's join chain takes the record:
|
|
162
|
+
# the reflection's own, the :through association's (and so on along
|
|
163
|
+
# the chain), and the source's of each :through. A chain that cannot
|
|
164
|
+
# be walked counts: the join would raise walking it.
|
|
165
|
+
def record_dependent?(reflection)
|
|
166
|
+
reflection.chain.any? do |link|
|
|
167
|
+
takes_record?(link.scope) || (link.through_reflection? && record_dependent?(link.source_reflection))
|
|
168
|
+
end
|
|
169
|
+
rescue StandardError
|
|
170
|
+
true
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def takes_record?(scope)
|
|
174
|
+
!scope.nil? && !scope.arity.zero?
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|