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,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'concurrent/map'
|
|
4
|
+
|
|
5
|
+
module PermissionForge
|
|
6
|
+
module Audit
|
|
7
|
+
# What makes a guarded attribute returned as stored inside a web request
|
|
8
|
+
# with nobody signed in visible (masked_attributes_nil_user_strategy
|
|
9
|
+
# :bypass; Configuration#masked_attributes_without_user_for answers
|
|
10
|
+
# :record): exactly the reads :hide_in_requests would hide — every
|
|
11
|
+
# guarded attribute of a masked model but its primary key and the raw
|
|
12
|
+
# paths, never the user model. The attribute guard (AttributeAccessGuard)
|
|
13
|
+
# calls .note for each such read.
|
|
14
|
+
#
|
|
15
|
+
# - The runtime event field.unmasked_without_user (group :bypass): the
|
|
16
|
+
# model, the attribute and the request (method, path, endpoint; request
|
|
17
|
+
# id and IP as event columns when the audit context has none), once per
|
|
18
|
+
# request per model and attribute (Runtime.once?). Nothing is built
|
|
19
|
+
# while nobody listens and the group is not stored (Audit::Gate).
|
|
20
|
+
# - One SECURITY warning per model and process in the log (Warnings.emit),
|
|
21
|
+
# so an app that stores no runtime event sees the gap too. Models are
|
|
22
|
+
# remembered by name (a development reload does not warn again), at
|
|
23
|
+
# most WARNED_LIMIT of them.
|
|
24
|
+
#
|
|
25
|
+
# Once the model has been warned about and nobody listens (the group not
|
|
26
|
+
# stored), there is nothing left to do: .pending? is false and the
|
|
27
|
+
# record's `attributes` returns the stored hash at once, as outside a
|
|
28
|
+
# request. A debugging read (inspect, MaskedAttributes::INSPECT_KEY) is
|
|
29
|
+
# not recorded. Neither ever raises into the read.
|
|
30
|
+
#
|
|
31
|
+
# @api private
|
|
32
|
+
module UnmaskedWithoutUser
|
|
33
|
+
EVENT = 'field.unmasked_without_user'
|
|
34
|
+
GATE = Gate.for(EVENT)
|
|
35
|
+
|
|
36
|
+
WARNED_LIMIT = 1_000
|
|
37
|
+
# The "Model#attribute" writes refused after a hidden read, logged once.
|
|
38
|
+
REFUSED = Concurrent::Map.new
|
|
39
|
+
|
|
40
|
+
# The names of the models warned about in this process: a frozen Hash
|
|
41
|
+
# replaced whole under WARNED_MUTEX, so the read on every guarded read
|
|
42
|
+
# takes no lock.
|
|
43
|
+
@warned = {}.freeze
|
|
44
|
+
WARNED_MUTEX = Mutex.new
|
|
45
|
+
|
|
46
|
+
class << self
|
|
47
|
+
# @return [Hash{String => true}] the names of the models warned about
|
|
48
|
+
attr_reader :warned
|
|
49
|
+
|
|
50
|
+
# @param model [Class, String, Symbol] the record's class (or the
|
|
51
|
+
# Resource name when no class is known)
|
|
52
|
+
# @param attribute [Symbol, String]
|
|
53
|
+
# @return [void]
|
|
54
|
+
def note(model, attribute)
|
|
55
|
+
heard = GATE.open?
|
|
56
|
+
name = model.is_a?(Class) ? model.name : model.to_s
|
|
57
|
+
first = !@warned.key?(name)
|
|
58
|
+
return unless (first || heard) && !ExecutionState[Concerns::MaskedAttributes::INSPECT_KEY]
|
|
59
|
+
|
|
60
|
+
warn_once(name, attribute) if first
|
|
61
|
+
publish(model, attribute) if heard
|
|
62
|
+
nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# A write refused because the record had read the attribute as hidden
|
|
66
|
+
# with nobody signed in (MaskedAttributes, write.masked_skipped
|
|
67
|
+
# `detected_by: hidden_without_user`): logged once per model and
|
|
68
|
+
# attribute and process, as the change may have been meant (a flow
|
|
69
|
+
# that changes a guarded attribute without a sign-in must sign in a
|
|
70
|
+
# system user).
|
|
71
|
+
#
|
|
72
|
+
# @param model [Class]
|
|
73
|
+
# @param attribute [Symbol, String]
|
|
74
|
+
# @param protection [Symbol] :skip or :raise
|
|
75
|
+
# @return [void]
|
|
76
|
+
def write_refused(model, attribute, protection)
|
|
77
|
+
key = "#{model.name}##{attribute}"
|
|
78
|
+
return if REFUSED.size >= WARNED_LIMIT || !REFUSED.put_if_absent(key, true).nil?
|
|
79
|
+
|
|
80
|
+
Warnings.emit("masked_attributes.write_refused_without_user.#{key}",
|
|
81
|
+
"#{key} was not written (masked_write_protection :#{protection}): the record had read it as " \
|
|
82
|
+
'hidden with nobody signed in, so the new value may come from the hidden one. A flow that ' \
|
|
83
|
+
'changes a guarded attribute without a sign-in (a webhook, a token link) must sign in a system ' \
|
|
84
|
+
'user. Logged once per attribute and process; write.masked_skipped records each refusal.',
|
|
85
|
+
severity: :config)
|
|
86
|
+
rescue StandardError
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Whether a read of `model` returned as stored in a web request still
|
|
91
|
+
# has something to do: the model's warning is still to come, or the
|
|
92
|
+
# event is stored or heard.
|
|
93
|
+
#
|
|
94
|
+
# @param model [Class, String]
|
|
95
|
+
# @return [Boolean]
|
|
96
|
+
def pending?(model)
|
|
97
|
+
!@warned.key?(Runtime.model_name(model)) || (GATE.open? && Audit.instrument?(EVENT))
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Forgets which models were warned about (tests; Audit.reset!).
|
|
101
|
+
# @return [void]
|
|
102
|
+
def reset!
|
|
103
|
+
WARNED_MUTEX.synchronize { @warned = {}.freeze }
|
|
104
|
+
REFUSED.clear
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
def publish(model, attribute)
|
|
110
|
+
return unless Audit.instrument?(EVENT)
|
|
111
|
+
|
|
112
|
+
name = Runtime.model_name(model)
|
|
113
|
+
attribute = attribute.to_s
|
|
114
|
+
return unless Runtime.once?(EVENT, name, attribute, nil)
|
|
115
|
+
|
|
116
|
+
with_request_context do
|
|
117
|
+
Audit.instrument(EVENT) do
|
|
118
|
+
{ record_type: Runtime.record_type(model), record_id: nil, subject_user_id: nil,
|
|
119
|
+
data: { 'model' => name, 'attribute' => attribute }.merge(WebRequest.details) }
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# The request id and IP of the web request, for an event recorded
|
|
125
|
+
# outside the admin's and the API's own audit context.
|
|
126
|
+
def with_request_context(&)
|
|
127
|
+
env = WebRequest.env
|
|
128
|
+
return yield if Context[:request_id] || env.nil?
|
|
129
|
+
|
|
130
|
+
request = ActionDispatch::Request.new(env)
|
|
131
|
+
Context.with(request_id: cut(request.request_id), ip: cut(RequestContext.remote_ip(request).first), &)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def cut(value)
|
|
135
|
+
value.presence && value.to_s[0, WebRequest::VALUE_LIMIT]
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def warn_once(name, attribute)
|
|
139
|
+
return unless remember(name)
|
|
140
|
+
|
|
141
|
+
Warnings.emit("masked_attributes.unmasked_without_user.#{name}", warning(name, attribute), severity: :security)
|
|
142
|
+
rescue StandardError
|
|
143
|
+
nil
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Adds the model's name; false when it was there already or the
|
|
147
|
+
# limit is reached.
|
|
148
|
+
def remember(name)
|
|
149
|
+
WARNED_MUTEX.synchronize do
|
|
150
|
+
next false if @warned.key?(name) || @warned.size >= WARNED_LIMIT
|
|
151
|
+
|
|
152
|
+
@warned = @warned.merge(name => true).freeze
|
|
153
|
+
true
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def warning(name, attribute)
|
|
158
|
+
request = WebRequest.details.values_at('method', 'path', 'endpoint').compact.join(' ')
|
|
159
|
+
"#{name}##{attribute} was read in a web request with nobody signed in " \
|
|
160
|
+
"(#{request.presence || 'a request'}) and returned as stored: masked_attributes_nil_user_strategy is " \
|
|
161
|
+
':bypass. Set it to :hide_in_requests to hide guarded attributes in web requests with nobody signed in ' \
|
|
162
|
+
'(jobs, rake and the console keep the stored values), or sign the request in before it reads masked ' \
|
|
163
|
+
'models. Logged once per model and process; the field.unmasked_without_user audit event (runtime ' \
|
|
164
|
+
'group :bypass) records each request.'
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -164,6 +164,7 @@ module PermissionForge
|
|
|
164
164
|
end
|
|
165
165
|
Context.reset!
|
|
166
166
|
Runtime.reset!
|
|
167
|
+
UnmaskedWithoutUser.reset!
|
|
167
168
|
end
|
|
168
169
|
|
|
169
170
|
# Installs the notification subscribers and the executor hook of the
|
|
@@ -280,22 +281,35 @@ module PermissionForge
|
|
|
280
281
|
end
|
|
281
282
|
end
|
|
282
283
|
|
|
284
|
+
# A database that does not answer or does not exist
|
|
285
|
+
# (DatabaseReach::UNREACHABLE_ERRORS) is "unchecked": nothing is
|
|
286
|
+
# recorded until it answers. Any other error of the check — a
|
|
287
|
+
# configuration error included — raises out of Audit.record, so the
|
|
288
|
+
# change it records rolls back.
|
|
283
289
|
def check_table(name)
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
false
|
|
290
|
+
cause = nil
|
|
291
|
+
exists = PermissionForge::DatabaseReach.table_exists?(PermissionForge::AuditEvent,
|
|
292
|
+
errors: PermissionForge::DatabaseReach::UNREACHABLE_ERRORS) { |e| cause = e }
|
|
293
|
+
if cause
|
|
294
|
+
warn_unchecked_table(name, cause)
|
|
295
|
+
return false
|
|
291
296
|
end
|
|
292
|
-
|
|
293
|
-
|
|
297
|
+
return @table_checks[name] = true if exists
|
|
298
|
+
|
|
299
|
+
@table_checks[name] = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
300
|
+
warn_missing_table(name) unless @warned_tables[name]
|
|
301
|
+
@warned_tables[name] = true
|
|
294
302
|
false
|
|
295
303
|
end
|
|
296
304
|
|
|
305
|
+
def warn_unchecked_table(name, cause)
|
|
306
|
+
PermissionForge::Warnings.emit(:audit_table_unchecked, "cannot check for the audit table '#{name}' (#{cause.class}): " \
|
|
307
|
+
'no audit events are recorded until the database answers.',
|
|
308
|
+
severity: :security)
|
|
309
|
+
end
|
|
310
|
+
|
|
297
311
|
def warn_missing_table(name)
|
|
298
|
-
|
|
312
|
+
PermissionForge::Warnings.emit(:audit_table_missing, BootWarnings.missing_table(name), severity: :security)
|
|
299
313
|
end
|
|
300
314
|
end
|
|
301
315
|
end
|
|
@@ -1,22 +1,50 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'warnings'
|
|
4
|
+
require_relative 'api_only_admin'
|
|
5
|
+
require_relative 'database_reach'
|
|
6
|
+
require_relative 'api_authorizations'
|
|
7
|
+
|
|
3
8
|
module PermissionForge
|
|
4
9
|
# What the engine checks while the host app boots: run (from
|
|
5
10
|
# config.after_initialize) and ensure_tenancy_gem! (from an initializer).
|
|
6
11
|
#
|
|
7
12
|
# The configuration checks need nothing. The database steps are advisory
|
|
8
13
|
# warnings or idempotent work, and they must never abort a boot: a Docker
|
|
9
|
-
# build precompiles assets with no database, a
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
+
# build precompiles assets with no database, a fresh `rails new
|
|
15
|
+
# --skip-kamal` app has no production database file until the deploy sets
|
|
16
|
+
# DATABASE_URL, a worker can start while the database fails over. The
|
|
17
|
+
# database is pinged once (DatabaseReach.problem: whatever connecting
|
|
18
|
+
# raises, an adapter's refusal of its configuration included); a step that
|
|
19
|
+
# loses it midway is skipped (DATABASE_STEP_ERRORS) and the next step runs.
|
|
20
|
+
# Anything else a step raises (a ConfigurationError) still stops the boot.
|
|
21
|
+
#
|
|
22
|
+
# A process that runs nothing but asset tasks (asset_build?:
|
|
23
|
+
# `bin/rails assets:precompile`, the Dockerfile's build step) runs the
|
|
24
|
+
# configuration checks only: it reads no data and serves nothing, and a
|
|
25
|
+
# build has no database to ping (an unreachable host could hold the build
|
|
26
|
+
# for a TCP timeout).
|
|
27
|
+
#
|
|
28
|
+
# Every warning goes through Warnings.emit, which decides the channel (the
|
|
29
|
+
# log; the terminal where the log is not read) and writes nothing while a
|
|
30
|
+
# generator boots the app (Warnings.generator_running?): `bin/rails
|
|
31
|
+
# generate permission_forge:install` boots before it writes the
|
|
32
|
+
# initializer, so its warnings described a configuration the app was about
|
|
33
|
+
# to stop having (the defaults, e.g. cache_backend :memory) — the first
|
|
34
|
+
# lines a new user read in log/development.log. Nothing is checked then
|
|
35
|
+
# either (run returns before the database steps). The next server, console
|
|
36
|
+
# or task boot checks the configuration the app has.
|
|
14
37
|
#
|
|
15
38
|
# @api private
|
|
16
39
|
module BootChecks
|
|
17
|
-
# Errors that mean "the database is not there (yet)"
|
|
18
|
-
# created, not migrated.
|
|
19
|
-
DATABASE_STEP_ERRORS =
|
|
40
|
+
# Errors that mean "the database is not there (yet)" once it answered the
|
|
41
|
+
# ping: gone, refused, not created, not migrated (DatabaseReach::READ_ERRORS).
|
|
42
|
+
DATABASE_STEP_ERRORS = DatabaseReach::READ_ERRORS
|
|
43
|
+
|
|
44
|
+
# The asset tasks of Propshaft and Sprockets (each boots the app through
|
|
45
|
+
# :environment). A process whose Rake top-level tasks are all among them
|
|
46
|
+
# is an asset build (asset_build?).
|
|
47
|
+
ASSET_TASKS = %w[assets:precompile assets:clean assets:clobber].freeze
|
|
20
48
|
|
|
21
49
|
# The boot steps that read the database, in order. Each runs on its
|
|
22
50
|
# own: one that loses the database is skipped (database_step).
|
|
@@ -34,16 +62,47 @@ module PermissionForge
|
|
|
34
62
|
masked_attributes: ->(_config) { MaskingSync.boot! }
|
|
35
63
|
}.freeze
|
|
36
64
|
|
|
65
|
+
# The commands `bin/rails generate` and `bin/rails destroy` run (the one
|
|
66
|
+
# definition is the emitter's: Warnings::GENERATOR_COMMANDS).
|
|
67
|
+
GENERATOR_COMMANDS = Warnings::GENERATOR_COMMANDS
|
|
68
|
+
|
|
37
69
|
class << self
|
|
38
70
|
# @param config [Configuration]
|
|
39
71
|
def run(config)
|
|
40
72
|
config.validate_gdpr_explicit_choice!
|
|
73
|
+
return MaskingSync.reset! if generator_running?
|
|
74
|
+
|
|
41
75
|
warn_configuration(config)
|
|
42
|
-
return
|
|
76
|
+
return MaskingSync.reset! if asset_build?
|
|
77
|
+
|
|
78
|
+
problem = database_problem
|
|
79
|
+
return database_unreachable(problem) if problem
|
|
43
80
|
|
|
44
81
|
DATABASE_STEPS.each { |name, step| database_step(name) { step.call(config) } }
|
|
45
82
|
end
|
|
46
83
|
|
|
84
|
+
# Whether this boot is that of a generator (`bin/rails generate` or
|
|
85
|
+
# `bin/rails destroy`): Warnings.generator_running?, which also keeps
|
|
86
|
+
# every warning quiet then.
|
|
87
|
+
#
|
|
88
|
+
# @return [Boolean]
|
|
89
|
+
def generator_running?
|
|
90
|
+
Warnings.generator_running?
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Whether this process runs nothing but asset tasks (ASSET_TASKS): the
|
|
94
|
+
# tasks Rake was asked for on the command line (`bin/rails
|
|
95
|
+
# assets:precompile`), not a task that only depends on one. False
|
|
96
|
+
# outside Rake.
|
|
97
|
+
#
|
|
98
|
+
# @return [Boolean]
|
|
99
|
+
def asset_build?
|
|
100
|
+
return false unless defined?(::Rake.application)
|
|
101
|
+
|
|
102
|
+
tasks = ::Rake.application.top_level_tasks.map { |task| task.sub(/\[.*\z/m, '') }
|
|
103
|
+
tasks.any? && tasks.all? { |task| ASSET_TASKS.include?(task) }
|
|
104
|
+
end
|
|
105
|
+
|
|
47
106
|
# HasTenantScoping silently skips the scope when the gem is absent,
|
|
48
107
|
# which would leave every tenant's permission data visible to every
|
|
49
108
|
# other tenant — refuse to boot instead.
|
|
@@ -71,13 +130,13 @@ module PermissionForge
|
|
|
71
130
|
return unless conditional_sets.exists?
|
|
72
131
|
return unless resolved_context(config).empty?
|
|
73
132
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
133
|
+
Warnings.emit(:conditional_sets_without_context,
|
|
134
|
+
"#{conditional_sets.count} permission set(s) use activation conditions " \
|
|
135
|
+
'(session flag, allowed_ips, or login hours) but request_context_resolver ' \
|
|
136
|
+
'returns empty context. These sets will evaluate as unconditional ' \
|
|
137
|
+
'(ALWAYS ACTIVE) — IP/hours/consent restrictions are NOT enforced. ' \
|
|
138
|
+
'Configure config.request_context_resolver, or set ' \
|
|
139
|
+
'config.conditional_sets_strict_mode = true to fail closed.', severity: :security)
|
|
81
140
|
rescue ActiveRecord::StatementInvalid
|
|
82
141
|
# Tables don't exist yet (pre-migration)
|
|
83
142
|
end
|
|
@@ -87,88 +146,146 @@ module PermissionForge
|
|
|
87
146
|
return if config.api_require_authorization
|
|
88
147
|
return if config.api_action_authorization && config.api_assignment_authorization
|
|
89
148
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
149
|
+
Warnings.emit(:api_authorization_gap,
|
|
150
|
+
'API is enabled, config.api_require_authorization is false and ' \
|
|
151
|
+
'api_action_authorization / api_assignment_authorization is not set. ' \
|
|
152
|
+
'api_auth only proves identity — any authenticated caller can create ' \
|
|
153
|
+
'permission sets and assign them (privilege escalation). Configure both ' \
|
|
154
|
+
'authorization lambdas, or remove api_require_authorization = false ' \
|
|
155
|
+
'(the default, true, denies mutations until they are set).', severity: :security, audience: :server)
|
|
97
156
|
end
|
|
98
157
|
|
|
99
|
-
#
|
|
100
|
-
#
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
158
|
+
# The API is on and api_auth admits callers, but a lambda the API
|
|
159
|
+
# needs is unset (ApiAuthorizations.unset): what it guards is refused
|
|
160
|
+
# (403). It fails closed; the note says so before the first refused
|
|
161
|
+
# request does. Under api_require_authorization = false the missing
|
|
162
|
+
# change and assignment lambdas are api_authorization_gap's instead.
|
|
163
|
+
#
|
|
164
|
+
# @param config [Configuration]
|
|
165
|
+
def warn_api_authorizations_unset(config)
|
|
166
|
+
lines = ApiAuthorizations.lines(config)
|
|
167
|
+
return if lines.empty?
|
|
168
|
+
|
|
169
|
+
Warnings.emit(:api_authorization_unset,
|
|
170
|
+
'The JSON API is on and api_auth admits callers, but these authorization lambdas are not ' \
|
|
171
|
+
"set, so the API refuses (403, naming the option) what each guards — #{lines.join('; ')}. " \
|
|
172
|
+
'Set each one in config/initializers/permission_forge.rb (docs/admin-api.md#requiring-authorization-lambdas).',
|
|
173
|
+
audience: :server)
|
|
174
|
+
end
|
|
104
175
|
|
|
105
|
-
|
|
106
|
-
|
|
176
|
+
# One query for the distinct names of the rows in the clear that reach
|
|
177
|
+
# someone's hash (in effect, in a granting set; no record is built),
|
|
178
|
+
# and one more for the resource of the first match. What counts as
|
|
179
|
+
# personal data is PermissionForge::SensitiveColumns' answer, as for
|
|
180
|
+
# the GDPR audit and the admin.
|
|
181
|
+
def warn_unmasked_gdpr_fields(config)
|
|
182
|
+
sensitive = PermissionForge::SensitiveColumns.new(configuration: config)
|
|
183
|
+
in_clear = PermissionForge::ResourceField.in_effect.merge(PermissionForge::PermissionSet.granting).exposing_unacknowledged
|
|
184
|
+
name, match = first_sensitive(in_clear.distinct.pluck(:name), sensitive)
|
|
107
185
|
return unless name
|
|
108
186
|
|
|
109
|
-
resource =
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
187
|
+
resource = in_clear.where(name: name).first&.resource
|
|
188
|
+
Warnings.emit(:unmasked_gdpr_fields,
|
|
189
|
+
"GDPR warning: field '#{name}' on resource '#{resource&.name}' #{matched_by(match)} " \
|
|
190
|
+
'but is not masked. Consider enabling gdpr_defaults or masking this field manually.')
|
|
113
191
|
rescue ActiveRecord::StatementInvalid
|
|
114
192
|
# Table doesn't exist yet (pre-migration)
|
|
115
193
|
end
|
|
116
194
|
|
|
195
|
+
# The admin screens in a `rails new --api` application: without the
|
|
196
|
+
# cookies, session and flash middlewares every admin page that renders
|
|
197
|
+
# a form is a 500 (ApiOnlyAdmin says which ones to add). Skipped when
|
|
198
|
+
# config.api_only_admin_warning is false.
|
|
199
|
+
#
|
|
200
|
+
# @param application [Rails::Application]
|
|
201
|
+
# @param config [Configuration]
|
|
202
|
+
def warn_api_only_admin(application = Rails.application, config: PermissionForge.configuration)
|
|
203
|
+
return unless config.api_only_admin_warning
|
|
204
|
+
|
|
205
|
+
missing = ApiOnlyAdmin.missing_middlewares(application)
|
|
206
|
+
return if missing.empty?
|
|
207
|
+
|
|
208
|
+
Warnings.emit(:api_only_admin, ApiOnlyAdmin.message(missing), severity: :config, audience: :server)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# The engine's after_routes_loaded hook: the api_only_admin warning,
|
|
212
|
+
# once per process (a development reload draws the routes again).
|
|
213
|
+
#
|
|
214
|
+
# @param application [Rails::Application]
|
|
215
|
+
def routes_loaded(application)
|
|
216
|
+
return if @routes_checked
|
|
217
|
+
|
|
218
|
+
@routes_checked = true
|
|
219
|
+
warn_api_only_admin(application)
|
|
220
|
+
end
|
|
221
|
+
|
|
117
222
|
private
|
|
118
223
|
|
|
119
|
-
#
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
224
|
+
# [name, SensitiveColumns::Match] of the first name that looks like personal data.
|
|
225
|
+
def first_sensitive(names, sensitive)
|
|
226
|
+
names.lazy.filter_map { |name| (match = sensitive.match(name)) && [name, match] }.first
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# What made the field personal data (SensitiveColumns::Match#source).
|
|
230
|
+
def matched_by(match)
|
|
231
|
+
match.filter_parameter? ? "is named by the application's filter_parameters" : "matches the GDPR pattern '#{match.pattern}'"
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# What connecting to the gem's database raised, nil when it answered
|
|
235
|
+
# (DatabaseReach.problem).
|
|
236
|
+
#
|
|
237
|
+
# @return [Exception, nil]
|
|
238
|
+
def database_problem
|
|
239
|
+
DatabaseReach.problem
|
|
127
240
|
end
|
|
128
241
|
|
|
129
242
|
def database_step(name)
|
|
130
243
|
yield
|
|
131
244
|
rescue *DATABASE_STEP_ERRORS => e
|
|
132
|
-
|
|
245
|
+
Warnings.emit(:"boot_check_skipped.#{name}",
|
|
246
|
+
"Boot check #{name} skipped: the database could not be read (#{e.class}).")
|
|
133
247
|
end
|
|
134
248
|
|
|
135
249
|
# The boot step did not run: the first request or job that reaches the
|
|
136
|
-
# database applies auto-applied masking (MaskingSync).
|
|
137
|
-
|
|
250
|
+
# database applies auto-applied masking (MaskingSync). The cause is
|
|
251
|
+
# named: "not configured" (an adapter's ArgumentError) reads otherwise
|
|
252
|
+
# than "refused". The ping loads the gem's models (and Active Record
|
|
253
|
+
# with them, as Rails' lazy load would): an error raised there — a
|
|
254
|
+
# host's on_load(:active_record) hook — is named the same way.
|
|
255
|
+
def database_unreachable(problem)
|
|
138
256
|
MaskingSync.reset!
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
257
|
+
Warnings.emit(:database_unreachable,
|
|
258
|
+
"The database could not be reached at boot, or the gem's models could not be loaded " \
|
|
259
|
+
"(#{problem.class}: #{problem.message.lines.first&.strip}); " \
|
|
260
|
+
'the boot checks that read it (security warnings) were skipped. Auto-applied masking is ' \
|
|
261
|
+
'applied at the first request or job that reaches the database.')
|
|
142
262
|
end
|
|
143
263
|
|
|
144
264
|
# The boot warnings that only read the configuration.
|
|
145
265
|
def warn_configuration(config)
|
|
146
|
-
|
|
266
|
+
CacheWarnings.call(config)
|
|
147
267
|
warn_basic_auth_without_credentials(config)
|
|
148
268
|
warn_api_authorization_gap(config)
|
|
149
|
-
config
|
|
269
|
+
warn_api_authorizations_unset(config)
|
|
270
|
+
config.tenantless_admin_warning&.then { |message| warn_tenantless_admin(message) }
|
|
150
271
|
QueryScopingWarnings.warn_unscoped_detection_raise_in_production(config)
|
|
272
|
+
WebRequest::StackCheck.check(config)
|
|
151
273
|
end
|
|
152
274
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
'Permissions will be inconsistent across Puma workers. ' \
|
|
158
|
-
'Configure config.cache_backend = :rails_cache with Redis, Memcached, or SolidCache.'
|
|
275
|
+
# AdminUiSettings#tenantless_admin_warning is the text; the emitter
|
|
276
|
+
# puts the prefix on.
|
|
277
|
+
def warn_tenantless_admin(message)
|
|
278
|
+
Warnings.emit(:tenantless_admin, message, severity: :security, audience: :server)
|
|
159
279
|
end
|
|
160
280
|
|
|
161
281
|
def warn_basic_auth_without_credentials(config)
|
|
162
282
|
return unless config.http_basic_auth_enabled
|
|
163
283
|
return if config.http_basic_auth_user.present? && config.http_basic_auth_password.present?
|
|
164
284
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
def gdpr_match?(field_name, patterns)
|
|
171
|
-
patterns.any? { |regex, _| field_name.match?(regex) }
|
|
285
|
+
Warnings.emit(:basic_auth_without_credentials,
|
|
286
|
+
'HTTP Basic Auth is enabled but credentials are not configured. The admin UI will return ' \
|
|
287
|
+
'401 for all requests. Set config.http_basic_auth_user and config.http_basic_auth_password.',
|
|
288
|
+
audience: :server)
|
|
172
289
|
end
|
|
173
290
|
|
|
174
291
|
# Taken as the per-read selection takes it: a Hash, or anything with
|
|
@@ -46,6 +46,16 @@ module PermissionForge
|
|
|
46
46
|
false
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
+
# Whether the store keeps what is written to it. A store that keeps
|
|
50
|
+
# nothing (cache_backend = :null, a :null_store) caches no permission
|
|
51
|
+
# hash, and cannot carry the token MaskingSync tells other processes
|
|
52
|
+
# about a newly masked model with.
|
|
53
|
+
#
|
|
54
|
+
# @return [Boolean]
|
|
55
|
+
def keeps_entries?
|
|
56
|
+
true
|
|
57
|
+
end
|
|
58
|
+
|
|
49
59
|
# Writes an entry that never expires — not even under a default expiry
|
|
50
60
|
# the store applies to every entry (a Rails cache configured with
|
|
51
61
|
# expires_in). Here a write without a TTL, which is that already.
|