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,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
|
|
5
|
+
module PermissionForge
|
|
6
|
+
# Generators of the gem's own documentation (development only; nothing here
|
|
7
|
+
# runs in a host application).
|
|
8
|
+
#
|
|
9
|
+
# @api private
|
|
10
|
+
module Docs
|
|
11
|
+
# The "Option reference" of docs/configuration.md, rendered from the code:
|
|
12
|
+
# Admin::SettingsCatalog::DEFINITIONS give the sections, the options and
|
|
13
|
+
# their order (the admin's Settings page), Configuration.new the defaults,
|
|
14
|
+
# Configuration::StrategyValues::ALLOWED what a strategy option takes, and
|
|
15
|
+
# the en locale the section titles and the first text of an option that
|
|
16
|
+
# has none yet. Every other public writer of Configuration follows under
|
|
17
|
+
# "Other options". The prose under each heading is kept from the document
|
|
18
|
+
# as it stands; the headings, their order and the `**Default:**` lines are
|
|
19
|
+
# regenerated (`bundle exec rake docs:configuration`;
|
|
20
|
+
# docs/configuration_reference_test.rb fails while the file is stale).
|
|
21
|
+
#
|
|
22
|
+
# @api private
|
|
23
|
+
class ConfigurationReference
|
|
24
|
+
START_MARK = '<!-- option-reference:start -->'
|
|
25
|
+
END_MARK = '<!-- option-reference:end -->'
|
|
26
|
+
NOTE = "<!-- Generated by `bundle exec rake docs:configuration` from SettingsCatalog and Configuration.new.\n " \
|
|
27
|
+
"Edit the text under a heading here (or settings.items.<option> in the en locale for a new option):\n " \
|
|
28
|
+
'the headings, their order and the **Default:** lines are regenerated. -->'
|
|
29
|
+
GENERATED_LINE = /\A\*\*Default:\*\*/
|
|
30
|
+
OTHER_SECTION = 'Other options'
|
|
31
|
+
PLACEHOLDER = '_Describe this option._'
|
|
32
|
+
LOCALE = File.expand_path('../../../config/locales/permission_forge.en.yml', __dir__)
|
|
33
|
+
|
|
34
|
+
# One `###` section of the reference: its title and the options under it.
|
|
35
|
+
Section = Struct.new(:title, :keys) do
|
|
36
|
+
def text(reference, existing)
|
|
37
|
+
intro = existing.section(title)
|
|
38
|
+
"### #{title}\n\n#{"#{intro}\n\n" unless intro.empty?}#{keys.map { |key| reference.entry(key, existing) }.join}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
attr_reader :document
|
|
43
|
+
|
|
44
|
+
# @param document [String] docs/configuration.md as it stands
|
|
45
|
+
def initialize(document:, configuration: Configuration.new, definitions: Admin::SettingsCatalog::DEFINITIONS,
|
|
46
|
+
locale: LOCALE)
|
|
47
|
+
@document = document
|
|
48
|
+
@configuration = configuration
|
|
49
|
+
@definitions = definitions
|
|
50
|
+
@locale = YAML.load_file(locale).dig('en', 'permission_forge', 'settings')
|
|
51
|
+
@values = Admin::SettingsValues.new
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# The document with its reference regenerated.
|
|
55
|
+
def render
|
|
56
|
+
before, region, after = split
|
|
57
|
+
existing = ExistingProse.new(region)
|
|
58
|
+
"#{before}#{START_MARK}\n#{NOTE}\n\n#{sections.map { |section| section.text(self, existing) }.join}#{END_MARK}#{after}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def stale? = render != document
|
|
62
|
+
|
|
63
|
+
# The option headings the reference holds, in order.
|
|
64
|
+
def keys = sections.flat_map(&:keys)
|
|
65
|
+
|
|
66
|
+
# One option's entry: heading, the generated default line, the text.
|
|
67
|
+
def entry(key, existing)
|
|
68
|
+
"#### #{key}\n\n#{default_line(key)}\n\n#{option_text(key, existing)}\n\n"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
# The document before the start mark, the region between the marks, the rest.
|
|
74
|
+
def split
|
|
75
|
+
start = document.index(START_MARK) or raise ArgumentError, "#{START_MARK} not found in the document"
|
|
76
|
+
stop = document.index(END_MARK, start) or raise ArgumentError, "#{END_MARK} not found in the document"
|
|
77
|
+
[document[0...start], document[(start + START_MARK.length)...stop], document[(stop + END_MARK.length)..]]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def sections
|
|
81
|
+
@sections ||= begin
|
|
82
|
+
by_section = @definitions.group_by(&:section)
|
|
83
|
+
catalog = Admin::SettingsCatalog::SECTIONS.map { |key| Section.new(@locale['sections'].fetch(key), by_section.fetch(key, []).map(&:key)) }
|
|
84
|
+
others = writers - @definitions.map(&:key)
|
|
85
|
+
others.empty? ? catalog : catalog << Section.new(OTHER_SECTION, others)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def writers
|
|
90
|
+
(@configuration.class.public_instance_methods - Object.public_instance_methods)
|
|
91
|
+
.grep(/\A\w+=\z/).map { |name| name.to_s.chomp('=') }.sort
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# The text under an option: what the document says, else the Settings page's name and description.
|
|
95
|
+
def option_text(key, existing)
|
|
96
|
+
text = existing.option(key)
|
|
97
|
+
return text unless text.empty?
|
|
98
|
+
|
|
99
|
+
name, desc = @locale['items'].fetch(key, {}).values_at('name', 'desc')
|
|
100
|
+
name ? "**#{name.sub(/\.\z/, '')}.** #{desc}" : PLACEHOLDER
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def default_line(key)
|
|
104
|
+
takes = takes_text(key)
|
|
105
|
+
"**Default:** #{default_text(key)}#{" Takes #{takes}." if takes}"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def takes_text(key)
|
|
109
|
+
allowed = Configuration::StrategyValues::ALLOWED[key.to_sym] or return
|
|
110
|
+
values = allowed.map { |value| "`#{value.inspect}`" }
|
|
111
|
+
list = values.size > 1 ? "#{values[0..-2].join(', ')} or #{values.last}" : values.first
|
|
112
|
+
"#{list} (or its String)#{', or nil' if Configuration::StrategyValues::BLANK_ALLOWED.include?(key.to_sym)}"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def default_text(key)
|
|
116
|
+
value = @configuration.public_send(key)
|
|
117
|
+
case value
|
|
118
|
+
when nil then nil_text(definition(key))
|
|
119
|
+
when Proc then "the gem's own lambda"
|
|
120
|
+
when Hash then hash_text(key, value)
|
|
121
|
+
else "`#{@values.ruby_literal(value)}`"
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def definition(key) = @definitions.find { |candidate| candidate.key == key }
|
|
126
|
+
|
|
127
|
+
def nil_text(definition)
|
|
128
|
+
case definition&.nil_label
|
|
129
|
+
when :default then '`nil` (the built-in behaviour)'
|
|
130
|
+
when :disabled then '`nil` (off)'
|
|
131
|
+
else '`nil`'
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def hash_text(key, value)
|
|
136
|
+
return '`{}`' if value.empty?
|
|
137
|
+
return '`PermissionForge::Configuration::DEFAULT_GDPR_FIELD_PATTERNS`' if value == Configuration::DEFAULT_GDPR_FIELD_PATTERNS
|
|
138
|
+
|
|
139
|
+
pairs = @values.normalize(definition(key) || Admin::SettingsCatalog.d(key, 'other', :map), value)
|
|
140
|
+
pairs.map { |pattern, target| "`#{pattern}` (#{target})" }.join(', ')
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# What the document says under each heading of the region: the section
|
|
144
|
+
# intros (keyed by title) and the option texts (keyed by option), the
|
|
145
|
+
# generated lines and the note left out.
|
|
146
|
+
class ExistingProse
|
|
147
|
+
def initialize(region)
|
|
148
|
+
@sections = Hash.new { |hash, key| hash[key] = +'' }
|
|
149
|
+
@options = Hash.new { |hash, key| hash[key] = +'' }
|
|
150
|
+
collect(region.sub(/\A\s*<!--.*?-->/m, ''))
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def section(title) = @sections[title].strip
|
|
154
|
+
def option(key) = @options[key].strip
|
|
155
|
+
|
|
156
|
+
private
|
|
157
|
+
|
|
158
|
+
def collect(region)
|
|
159
|
+
bucket = +''
|
|
160
|
+
region.each_line do |line|
|
|
161
|
+
case line.chomp
|
|
162
|
+
when /\A### (.+)\z/ then bucket = @sections[Regexp.last_match(1).strip]
|
|
163
|
+
when /\A#### (\w+)\s*\z/ then bucket = @options[Regexp.last_match(1)]
|
|
164
|
+
when ConfigurationReference::GENERATED_LINE then next
|
|
165
|
+
else bucket << line
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
# The one reader of a model's Active Record encryption declarations
|
|
5
|
+
# (`encrypts :email`): which columns hold ciphertext. Rails keeps the
|
|
6
|
+
# column's name, so nothing in the schema says so — the column of an
|
|
7
|
+
# encrypted attribute stores `{"p":"…","h":{…}}`, and SQL that compares it
|
|
8
|
+
# with a value compares the ciphertext:
|
|
9
|
+
#
|
|
10
|
+
# - a record rule cannot compare one (Operators::RuleColumn#encrypted?,
|
|
11
|
+
# Operators::ValueTyping: every built-in operator that takes a value
|
|
12
|
+
# matches nothing; is_null / is_not_null work, as the ciphertext is NULL
|
|
13
|
+
# exactly when the value is; RuleProblem :encrypted_column), whichever
|
|
14
|
+
# table of the rule's query it is in ({.models_on}: a table the query
|
|
15
|
+
# joins that is on no model along the rule's path);
|
|
16
|
+
# - the admin's people search leaves one out of its LIKE, and bulk matching
|
|
17
|
+
# of its lower-cased equality; a deterministically encrypted one matches
|
|
18
|
+
# a whole value exactly (`where(email: value)` encrypts the value);
|
|
19
|
+
# - a person's label reads one decrypted (Admin::PersonLabel).
|
|
20
|
+
#
|
|
21
|
+
# Rails' `encrypted_attributes` (no rdoc; a canary watches it) is nil on a
|
|
22
|
+
# model that encrypts nothing and holds Symbols — an `ignore_case`
|
|
23
|
+
# attribute's `original_<name>` column among them. Under single-table
|
|
24
|
+
# inheritance it is per class, and a subclass's `encrypts` adds to its
|
|
25
|
+
# parent's Set when the parent has one (`||= Set.new`): the parent's
|
|
26
|
+
# answer depends on what encrypts what, and in which order. A column holds
|
|
27
|
+
# ciphertext when any class that stores rows in it says so, so the answer
|
|
28
|
+
# is the hierarchy's on the model's table ({.family}): the class, its base
|
|
29
|
+
# class and every loaded class of the hierarchy on that table. A subclass
|
|
30
|
+
# Rails has not loaded yet (lazy loading in development) is not seen.
|
|
31
|
+
#
|
|
32
|
+
# The value Rails reads from a column it cannot decrypt is no value
|
|
33
|
+
# ({.decrypt}): under `support_unencrypted_data` Rails answers with the
|
|
34
|
+
# stored ciphertext instead of raising.
|
|
35
|
+
#
|
|
36
|
+
# @api private
|
|
37
|
+
module EncryptedAttributes
|
|
38
|
+
module_function
|
|
39
|
+
|
|
40
|
+
# Whether the column holds ciphertext: an attribute of that name is
|
|
41
|
+
# declared with `encrypts` by the model or a class of its hierarchy on
|
|
42
|
+
# its table ({.family}).
|
|
43
|
+
#
|
|
44
|
+
# @param model [Class, nil] a model class (anything else has none)
|
|
45
|
+
# @param name [String, Symbol] a column or attribute name, as the model has it
|
|
46
|
+
# @return [Boolean]
|
|
47
|
+
def encrypted?(model, name)
|
|
48
|
+
names(model).include?(name.to_s.to_sym)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Whether the attribute is encrypted deterministically: the same value
|
|
52
|
+
# always gives the same ciphertext, so `where(name => value)` finds it.
|
|
53
|
+
#
|
|
54
|
+
# @param model [Class, nil]
|
|
55
|
+
# @param name [String, Symbol]
|
|
56
|
+
# @return [Boolean]
|
|
57
|
+
def deterministic?(model, name)
|
|
58
|
+
encrypted?(model, name) && Array(model.deterministic_encrypted_attributes).include?(name.to_s.to_sym)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @param model [Class, nil]
|
|
62
|
+
# @return [Array<Symbol>] the encrypted attributes of the model's
|
|
63
|
+
# hierarchy on its table ({.family}; none for a non-model)
|
|
64
|
+
def names(model)
|
|
65
|
+
return [] unless model.respond_to?(:encrypted_attributes)
|
|
66
|
+
|
|
67
|
+
family(model).flat_map { |klass| Array(klass.encrypted_attributes) }.uniq
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# The classes that store rows in the model's table: the model, its base
|
|
71
|
+
# class and every loaded descendant of the base class on the same table
|
|
72
|
+
# (single-table inheritance). Just the model for an abstract class or a
|
|
73
|
+
# class without a base class.
|
|
74
|
+
#
|
|
75
|
+
# @param model [Class]
|
|
76
|
+
# @return [Array<Class>]
|
|
77
|
+
def family(model)
|
|
78
|
+
return [model] unless model.respond_to?(:base_class) && !model.abstract_class?
|
|
79
|
+
|
|
80
|
+
base = model.base_class
|
|
81
|
+
table = model.table_name
|
|
82
|
+
[model, base, *base.descendants.select { |klass| !klass.abstract_class? && klass.table_name == table }].uniq
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Every loaded model class that stores rows in a table of that name:
|
|
86
|
+
# what a table a query joins stands for when it is on no model the
|
|
87
|
+
# caller knows (a `has_many :through`'s join table, a table a default
|
|
88
|
+
# scope or a host's join brings in: Operators::EncryptedAlias). Any
|
|
89
|
+
# loaded, non-abstract Active Record class whose table (its last
|
|
90
|
+
# segment, without a schema) is the name — the classes of a
|
|
91
|
+
# single-table hierarchy among them, so {.names} of any one of them
|
|
92
|
+
# covers the hierarchy. A class the application has not loaded yet
|
|
93
|
+
# (lazy loading in development) is not seen; nor is the database a
|
|
94
|
+
# class is on told apart (a table of that name in another database
|
|
95
|
+
# counts too: fail closed).
|
|
96
|
+
#
|
|
97
|
+
# @param table [String] the table's name, as the query gives it
|
|
98
|
+
# @param exact [Boolean] compare names as written (PostgreSQL), else
|
|
99
|
+
# without regard to case (Operators::RuleColumn.same_name?)
|
|
100
|
+
# @return [Array<Class>]
|
|
101
|
+
def models_on(table, exact: false)
|
|
102
|
+
ActiveRecord::Base.descendants.select do |klass|
|
|
103
|
+
next false if klass.abstract_class?
|
|
104
|
+
|
|
105
|
+
own = klass.table_name.to_s.split('.').last
|
|
106
|
+
own.present? && Operators::RuleColumn.same_name?(own, table.to_s, exact)
|
|
107
|
+
rescue StandardError
|
|
108
|
+
false # a class that cannot name its table (an anonymous one without `table_name`) stores rows in none
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Of `names` (column names), those that do not hold ciphertext: what SQL
|
|
113
|
+
# can compare as written.
|
|
114
|
+
#
|
|
115
|
+
# @param model [Class]
|
|
116
|
+
# @param names [Array<String>]
|
|
117
|
+
# @return [Array<String>]
|
|
118
|
+
def plain(model, names)
|
|
119
|
+
names.reject { |name| encrypted?(model, name) }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Of `names`, the deterministically encrypted ones.
|
|
123
|
+
#
|
|
124
|
+
# @param model [Class]
|
|
125
|
+
# @param names [Array<String>]
|
|
126
|
+
# @return [Array<String>]
|
|
127
|
+
def deterministic(model, names)
|
|
128
|
+
names.select { |name| deterministic?(model, name) }
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# The value a stored ciphertext holds, read through the attribute's type
|
|
132
|
+
# (past any reader the model overrides, as MaskedAttributes does); nil
|
|
133
|
+
# when this application cannot decrypt it (another key, not ciphertext).
|
|
134
|
+
# Under `support_unencrypted_data` Rails answers a value it cannot
|
|
135
|
+
# decrypt with the stored value itself: a result that is still
|
|
136
|
+
# ciphertext is nil too. A value stored in plaintext reads as written
|
|
137
|
+
# there (Rails' own reading).
|
|
138
|
+
#
|
|
139
|
+
# @param model [Class]
|
|
140
|
+
# @param name [String, Symbol]
|
|
141
|
+
# @param raw [Object] the value as stored (read_attribute_before_type_cast)
|
|
142
|
+
# @return [Object, nil]
|
|
143
|
+
def decrypt(model, name, raw)
|
|
144
|
+
value = model.type_for_attribute(name.to_s).deserialize(raw)
|
|
145
|
+
value unless value.is_a?(String) && ActiveRecord::Encryption.encryptor.encrypted?(value)
|
|
146
|
+
rescue ActiveRecord::Encryption::Errors::Base
|
|
147
|
+
nil
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
require_relative 'query_scoping_warnings'
|
|
2
|
+
require_relative 'cache_warnings'
|
|
2
3
|
require_relative 'boot_checks'
|
|
3
4
|
require_relative 'masking_sync'
|
|
4
5
|
require_relative 'masked_models'
|
|
6
|
+
require_relative 'admin_method_override'
|
|
5
7
|
|
|
6
8
|
module PermissionForge
|
|
7
9
|
# Wires the gem into the host application: initializers, the executor and
|
|
@@ -16,6 +18,41 @@ module PermissionForge
|
|
|
16
18
|
g.test_framework :minitest
|
|
17
19
|
end
|
|
18
20
|
|
|
21
|
+
# The admin's edit and delete forms POST with a hidden `_method`: the
|
|
22
|
+
# engine's own stack (it runs for the engine's routes only) turns them
|
|
23
|
+
# into PATCH / DELETE, so an API-only host, which has no
|
|
24
|
+
# Rack::MethodOverride, needs nothing for it. The JSON API's paths are
|
|
25
|
+
# left alone (AdminMethodOverride).
|
|
26
|
+
config.middleware.use PermissionForge::AdminMethodOverride
|
|
27
|
+
|
|
28
|
+
# Every request the host application serves is a web request
|
|
29
|
+
# (PermissionForge::WebRequest) from the top of its stack until the
|
|
30
|
+
# response is finished: masked_attributes_nil_user_strategy
|
|
31
|
+
# :hide_in_requests hides there, and :bypass records
|
|
32
|
+
# field.unmasked_without_user. At the top of the stack (unless a
|
|
33
|
+
# middleware is inserted above it with insert_before 0: StackCheck names
|
|
34
|
+
# it at boot), so that a middleware or a before_action that reads a model
|
|
35
|
+
# before anyone is signed in counts too.
|
|
36
|
+
config.app_middleware.unshift PermissionForge::WebRequest::Middleware
|
|
37
|
+
|
|
38
|
+
# A denial nobody rescues answers 403, not 500: the row-rule refusal of
|
|
39
|
+
# find_by / find_by! and the action guard's refusal (Rails' exceptions
|
|
40
|
+
# app renders the status: public/403.html when the host has one, else an
|
|
41
|
+
# empty body). A host's rescue_from runs first and still decides.
|
|
42
|
+
#
|
|
43
|
+
# Set as the gem loads, as ActiveRecord's railtie sets its own: before
|
|
44
|
+
# ActionDispatch copies the setting into ExceptionWrapper, and before the
|
|
45
|
+
# host's config/application.rb, whose entry for either name wins. Class
|
|
46
|
+
# names, not constants: nothing is loaded for this.
|
|
47
|
+
config.action_dispatch.rescue_responses.merge!(
|
|
48
|
+
'PermissionForge::Concerns::PermissionScopedFindable::NotAuthorizedError' => :forbidden,
|
|
49
|
+
'PermissionForge::ActionNotAuthorizedError' => :forbidden,
|
|
50
|
+
# A request value the admin cannot read (ParamShapes::InvalidParam, an
|
|
51
|
+
# ActionController::BadRequest): 400 as its parent, which Rails maps by
|
|
52
|
+
# exact class name only. The API answers it with a 422 itself.
|
|
53
|
+
'PermissionForge::ParamShapes::InvalidParam' => :bad_request
|
|
54
|
+
)
|
|
55
|
+
|
|
19
56
|
# The models (app/models/permission_forge) are autoloaded like any engine
|
|
20
57
|
# model: their class bodies read the configuration after the host's
|
|
21
58
|
# config/initializers, so nothing is re-declared after boot. No engine
|
|
@@ -47,7 +84,11 @@ module PermissionForge
|
|
|
47
84
|
new_filters = PermissionForge.gdpr_filter_parameters
|
|
48
85
|
app.config.filter_parameters += new_filters
|
|
49
86
|
|
|
50
|
-
|
|
87
|
+
# Not while a generator boots the app (BootChecks): the install
|
|
88
|
+
# generator's boot predates the initializer it writes.
|
|
89
|
+
unless PermissionForge::BootChecks.generator_running?
|
|
90
|
+
Rails.logger.info "[PermissionForge] Added #{new_filters.size} GDPR patterns to filter_parameters."
|
|
91
|
+
end
|
|
51
92
|
end
|
|
52
93
|
end
|
|
53
94
|
|
|
@@ -87,6 +128,10 @@ module PermissionForge
|
|
|
87
128
|
PermissionForge::BootChecks.run(PermissionForge.configuration)
|
|
88
129
|
end
|
|
89
130
|
|
|
131
|
+
# The checks that read the routes, once they are drawn: at boot, or at
|
|
132
|
+
# their first use where Rails 8 draws them lazily (eager_load off).
|
|
133
|
+
config.after_routes_loaded { |app| PermissionForge::BootChecks.routes_loaded(app) }
|
|
134
|
+
|
|
90
135
|
# A setting a model reads as it loads (user_class, audit_user_class,
|
|
91
136
|
# tenant_model) changed by the host's own after_initialize, once a
|
|
92
137
|
# production boot has eager-loaded the models: the classes keep what they
|
|
@@ -96,18 +141,5 @@ module PermissionForge
|
|
|
96
141
|
initializer 'permission_forge.model_bindings', after: :load_config_initializers do |app|
|
|
97
142
|
app.config.after_initialize { PermissionForge::ModelBindings.warn! }
|
|
98
143
|
end
|
|
99
|
-
|
|
100
|
-
# Deprecated: the masking machinery left the Engine (0.14). Removed in
|
|
101
|
-
# PermissionForge::Deprecation::HORIZON.
|
|
102
|
-
#
|
|
103
|
-
# @return [Boolean] what MaskedModels.apply returns
|
|
104
|
-
def self.apply_masked_attributes_to_managed_models(only: nil)
|
|
105
|
-
PermissionForge::Deprecation.warn(
|
|
106
|
-
'PermissionForge::Engine.apply_masked_attributes_to_managed_models is deprecated; ' \
|
|
107
|
-
'use PermissionForge::MaskedModels.apply(only:)',
|
|
108
|
-
callstack: caller_locations(1) # the host's call, not this method
|
|
109
|
-
)
|
|
110
|
-
PermissionForge::MaskedModels.apply(only: only)
|
|
111
|
-
end
|
|
112
144
|
end
|
|
113
145
|
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module ExecutionState
|
|
5
|
+
# What .set_until_restored hands back: puts back what the slot held when
|
|
6
|
+
# it was set — once, and only where it was set (ExecutionState.owner: the
|
|
7
|
+
# fiber of a :fiber slot, the execution context of an :execution slot).
|
|
8
|
+
# Called from anywhere else (a Rack server finishing the response on
|
|
9
|
+
# another thread) it does nothing there, and the slot stays set where it
|
|
10
|
+
# was.
|
|
11
|
+
#
|
|
12
|
+
# @api private
|
|
13
|
+
class Restore
|
|
14
|
+
# @param name [Symbol] a key of KEYS
|
|
15
|
+
# @param previous [Object, nil] what the slot held
|
|
16
|
+
def initialize(name, previous)
|
|
17
|
+
@name = name
|
|
18
|
+
@previous = previous
|
|
19
|
+
@owner = ExecutionState.owner(name)
|
|
20
|
+
@done = false
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Takes and ignores the arguments a `rack.response_finished` callback
|
|
24
|
+
# is given.
|
|
25
|
+
#
|
|
26
|
+
# @return [Boolean] whether it put the value back (false: done
|
|
27
|
+
# already, or called elsewhere)
|
|
28
|
+
def call(*)
|
|
29
|
+
return false if @done || !ExecutionState.owner(@name).equal?(@owner)
|
|
30
|
+
|
|
31
|
+
@done = true
|
|
32
|
+
ExecutionState.put_back(@name, @previous)
|
|
33
|
+
true
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class << self
|
|
38
|
+
# Sets the slot past the end of the current block: for a value that
|
|
39
|
+
# must outlive the call that sets it (a Rack middleware keeps it until
|
|
40
|
+
# the response is finished, after its #call returned).
|
|
41
|
+
#
|
|
42
|
+
# @param name [Symbol] a key of KEYS
|
|
43
|
+
# @return [Restore] puts back what the slot held
|
|
44
|
+
def set_until_restored(name, value)
|
|
45
|
+
restore = Restore.new(name, self[name])
|
|
46
|
+
self[name] = value
|
|
47
|
+
restore
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# A read-only reader of one slot, for a hot path that asks the same slot
|
|
51
|
+
# on every call without the lookup of #[] (WebRequest.active?): the
|
|
52
|
+
# store is looked up once, here.
|
|
53
|
+
#
|
|
54
|
+
# @param name [Symbol] a key of KEYS
|
|
55
|
+
# @return [Proc] returns what the slot holds
|
|
56
|
+
def reader(name)
|
|
57
|
+
store = storage(name)
|
|
58
|
+
-> { store[name] }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Puts `previous` back in the slot (nil empties it), as .with does when
|
|
62
|
+
# its block ends.
|
|
63
|
+
#
|
|
64
|
+
# @param name [Symbol] a key of KEYS
|
|
65
|
+
# @param previous [Object, nil]
|
|
66
|
+
# @return [void]
|
|
67
|
+
def put_back(name, previous)
|
|
68
|
+
store = storage(name)
|
|
69
|
+
previous.nil? ? store.delete(name) : store[name] = previous
|
|
70
|
+
nil
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -122,7 +122,11 @@ module PermissionForge
|
|
|
122
122
|
Key.new(:pf_audit_transaction_correlation, :execution, :clean,
|
|
123
123
|
'[transaction, id] of the correlation id minted for the outermost open transaction'),
|
|
124
124
|
Key.new(:pf_audit_runtime_once, :execution, :clean,
|
|
125
|
-
'Hash of the runtime audit events published once per request (Audit::Runtime.once?)')
|
|
125
|
+
'Hash of the runtime audit events published once per request (Audit::Runtime.once?)'),
|
|
126
|
+
Key.new(:pf_web_request, :execution, :keep,
|
|
127
|
+
'the Rack env of the web request in progress (WebRequest::Middleware, until the response is finished). ' \
|
|
128
|
+
'Execution: under :thread isolation every fiber of the request sees it. Seen elsewhere it hides more (fails closed); emptied ' \
|
|
129
|
+
'inside the request by a run completed there it would hide nothing: kept, the middleware puts it back')
|
|
126
130
|
].to_h { |key| [key.name, key] }.freeze
|
|
127
131
|
|
|
128
132
|
CLEANED = KEYS.values.select { |key| key.at_run_end == :clean }.map(&:name).freeze
|
|
@@ -227,6 +231,9 @@ module PermissionForge
|
|
|
227
231
|
end
|
|
228
232
|
end
|
|
229
233
|
|
|
234
|
+
# Where slot `name` is kept now, the fiber or the execution context (Restore compares it).
|
|
235
|
+
def owner(name) = storage(name).equal?(FiberStorage) ? Fiber.current : ActiveSupport::IsolatedExecutionState.context
|
|
236
|
+
|
|
230
237
|
# Empties every slot of this fiber and execution context: tests.
|
|
231
238
|
#
|
|
232
239
|
# @return [void]
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
# Maps the single "access level" of a field row to ResourceField flags and
|
|
5
|
+
# back. Granting sets offer Hidden / Masked / Read-only / Editable; muting
|
|
6
|
+
# sets offer Unchanged / Force mask / Hide, where the same flags mean
|
|
7
|
+
# "remove" (see PermissionHashBuilder#mute_single_field). The admin's
|
|
8
|
+
# controls, the ChangeSet, the effective-access views and the one row
|
|
9
|
+
# builder (ResourceFieldGenerator::RowBuilder) all read the tables here.
|
|
10
|
+
# Before 0.15.0 this lived at PermissionForge::Admin::FieldLevel, which is
|
|
11
|
+
# kept as an alias.
|
|
12
|
+
#
|
|
13
|
+
# @api private
|
|
14
|
+
module FieldLevel
|
|
15
|
+
GRANTING = {
|
|
16
|
+
'hidden' => { read_access: false, edit_access: false, mask_access: false },
|
|
17
|
+
'masked' => { read_access: true, edit_access: false, mask_access: true },
|
|
18
|
+
'read' => { read_access: true, edit_access: false, mask_access: false },
|
|
19
|
+
'edit' => { read_access: true, edit_access: true, mask_access: false }
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
MUTING = {
|
|
23
|
+
'leave' => { read_access: false, edit_access: false, mask_access: false },
|
|
24
|
+
'mask' => { read_access: false, edit_access: false, mask_access: true },
|
|
25
|
+
'hide' => { read_access: true, edit_access: true, mask_access: false }
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
# Ordered from most closed to most open (granting) — used to rank levels.
|
|
29
|
+
GRANTING_ORDER = %w[hidden masked read edit].freeze
|
|
30
|
+
|
|
31
|
+
module_function
|
|
32
|
+
|
|
33
|
+
def levels_for(muting:)
|
|
34
|
+
muting ? MUTING.keys : GRANTING.keys
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The level of a column with no setting: Unchanged in a Muting
|
|
38
|
+
# Permission Set (it mutes nothing: one state, one name), none in a
|
|
39
|
+
# granting one (unconfigured_field_strategy decides, and the row says so).
|
|
40
|
+
def unconfigured_level(muting:)
|
|
41
|
+
muting ? 'leave' : nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Whether a level is a row: every level of the set's kind, except
|
|
45
|
+
# Unchanged in a Muting Permission Set — it mutes nothing, which is what
|
|
46
|
+
# no row means. The one definition every path that creates rows reads
|
|
47
|
+
# (the row builder, the admin's presets and bulk add, the generator).
|
|
48
|
+
#
|
|
49
|
+
# @return [Boolean]
|
|
50
|
+
def row?(level, muting:)
|
|
51
|
+
valid?(level, muting: muting) && !(muting && level.to_s == 'leave')
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# The level a row gets on its Resource: on a granting set Editable needs
|
|
55
|
+
# the Resource's Edit (the admin's level control disables it, turning
|
|
56
|
+
# the Resource's Edit off cascades Editable rows to Read-only, the
|
|
57
|
+
# effective-access views cap it), so without it Editable is Read-only —
|
|
58
|
+
# the row says what the field allows. A muting set's levels take access
|
|
59
|
+
# away and are never capped. The one definition every path that writes a
|
|
60
|
+
# level reads: the row builder (after the privacy default, so an
|
|
61
|
+
# Editable personal-data field becomes Read-only, never Masked) and the
|
|
62
|
+
# Fields tab's level edits.
|
|
63
|
+
#
|
|
64
|
+
# @param level [String]
|
|
65
|
+
# @param resource_edit [Boolean] the Resource grants Edit
|
|
66
|
+
# @param muting [Boolean]
|
|
67
|
+
# @return [String]
|
|
68
|
+
def capped(level, resource_edit:, muting:)
|
|
69
|
+
level = level.to_s
|
|
70
|
+
level == 'edit' && !muting && !resource_edit ? 'read' : level
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def flags_for(level, muting:)
|
|
74
|
+
(muting ? MUTING : GRANTING).fetch(level.to_s) { raise ArgumentError, "Unknown field level: #{level.inspect}" }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def valid?(level, muting:)
|
|
78
|
+
(muting ? MUTING : GRANTING).key?(level.to_s)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Derives the level from a field's flags. Flag combinations the UI never
|
|
82
|
+
# writes (e.g. edit + mask) collapse to the closest level.
|
|
83
|
+
# What a muting row takes away, for provenance (the WHY panel): its
|
|
84
|
+
# level, except a row that mutes edit but not read (API / console
|
|
85
|
+
# only), which takes editing away and leaves the field readable:
|
|
86
|
+
# 'no_edit', a level no form offers.
|
|
87
|
+
def muting_provenance(field)
|
|
88
|
+
level = level_of(field, muting: true)
|
|
89
|
+
level == 'hide' && !field.read_access ? 'no_edit' : level
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def level_of(field, muting:)
|
|
93
|
+
read = field.read_access
|
|
94
|
+
edit = field.edit_access
|
|
95
|
+
mask = field.mask_access
|
|
96
|
+
return muting_level(read, edit, mask) if muting
|
|
97
|
+
|
|
98
|
+
return 'hidden' unless read
|
|
99
|
+
return 'masked' if mask
|
|
100
|
+
|
|
101
|
+
edit ? 'edit' : 'read'
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def muting_level(read, edit, mask)
|
|
105
|
+
return 'hide' if read || edit
|
|
106
|
+
|
|
107
|
+
mask ? 'mask' : 'leave'
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Level an engine field-permission entry resolves to
|
|
111
|
+
# ({ read_access:, edit_access:, mask_access: } from the permissions hash).
|
|
112
|
+
def effective_level(entry)
|
|
113
|
+
return 'hidden' if entry.nil? || !entry[:read_access]
|
|
114
|
+
return 'masked' if entry[:mask_access]
|
|
115
|
+
|
|
116
|
+
entry[:edit_access] ? 'edit' : 'read'
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def rank(level)
|
|
120
|
+
GRANTING_ORDER.index(level.to_s) || -1
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|