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
|
@@ -150,9 +150,17 @@ module PermissionForge
|
|
|
150
150
|
end
|
|
151
151
|
|
|
152
152
|
# saved_changes minus the bookkeeping columns, JSON-ready.
|
|
153
|
-
def audit_changes
|
|
154
|
-
|
|
155
|
-
|
|
153
|
+
def audit_changes = saved_changes.except(*BOOKKEEPING_COLUMNS).as_json
|
|
154
|
+
|
|
155
|
+
# Whether the last save wrote a change event: SaveNotice asks it.
|
|
156
|
+
def saved_audited_changes? = audit_changes.any?
|
|
157
|
+
|
|
158
|
+
# Whether saving now would write one (a new record always does): the
|
|
159
|
+
# admin saves a record only then (SaveNotice#pf_save_changed), so an
|
|
160
|
+
# unchanged submit runs no commit callback and invalidates nobody's
|
|
161
|
+
# cached permissions. Never asked by a callback: a second, unchanged
|
|
162
|
+
# save in a transaction must not drop the first one's invalidation.
|
|
163
|
+
def audited_changes_pending? = new_record? || changes_to_save.except(*BOOKKEEPING_COLUMNS).any?
|
|
156
164
|
|
|
157
165
|
# The Permission Set an event about this record belongs to, if any.
|
|
158
166
|
def audit_permission_set
|
|
@@ -226,10 +234,9 @@ module PermissionForge
|
|
|
226
234
|
end
|
|
227
235
|
|
|
228
236
|
def record_audit_event_for_update
|
|
229
|
-
|
|
230
|
-
return if changes.empty?
|
|
237
|
+
return unless saved_audited_changes?
|
|
231
238
|
|
|
232
|
-
record_audit_event(:update, audit_update_details.merge('changes' =>
|
|
239
|
+
record_audit_event(:update, audit_update_details.merge('changes' => audit_changes))
|
|
233
240
|
end
|
|
234
241
|
|
|
235
242
|
# Event data a model adds to its update events (the computed 'changes' wins).
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Concerns
|
|
5
|
+
# The one definition of "this record needs a name": a Permission Set, a
|
|
6
|
+
# group, a rule, a Resource, a field row and an action row. A name that
|
|
7
|
+
# is nil, empty or only whitespace is refused with
|
|
8
|
+
# `permission_forge.validations.name_blank` — in code, through the JSON
|
|
9
|
+
# API and in the admin alike.
|
|
10
|
+
#
|
|
11
|
+
# Checked for a new row and on a rename only: a row stored with a blank
|
|
12
|
+
# name before (written past the model, or before 0.15 for a rule) stays
|
|
13
|
+
# editable in its other attributes; the admin shows such a name as
|
|
14
|
+
# "Untitled …" in the page's language (Admin::RecordName).
|
|
15
|
+
#
|
|
16
|
+
# Field test round 8 found a rule saved with name "" or " " while sets
|
|
17
|
+
# and groups refused it: each model declared the rule itself, and the rule
|
|
18
|
+
# model never had it.
|
|
19
|
+
#
|
|
20
|
+
# @api private
|
|
21
|
+
module RequiresName
|
|
22
|
+
extend ActiveSupport::Concern
|
|
23
|
+
|
|
24
|
+
included do
|
|
25
|
+
validates :name, presence: { message: ->(*) { I18n.t('permission_forge.validations.name_blank') } },
|
|
26
|
+
if: :name_check_due?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# A new row, or a change to its name.
|
|
32
|
+
def name_check_due?
|
|
33
|
+
new_record? || will_save_change_to_name?
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -24,6 +24,7 @@ module PermissionForge
|
|
|
24
24
|
}.freeze
|
|
25
25
|
|
|
26
26
|
included do
|
|
27
|
+
before_validation :refuse_nul_bytes
|
|
27
28
|
validate :validate_column_lengths
|
|
28
29
|
end
|
|
29
30
|
|
|
@@ -38,6 +39,29 @@ module PermissionForge
|
|
|
38
39
|
|
|
39
40
|
private
|
|
40
41
|
|
|
42
|
+
# A NUL byte in a string or text attribute being written: PostgreSQL
|
|
43
|
+
# cannot store one (its driver raises on any statement carrying it, a
|
|
44
|
+
# uniqueness check's included) and SQLite reads the statement cut
|
|
45
|
+
# short. Refused before the other validators run — they may query
|
|
46
|
+
# with the value — so a save is a validation error, never a raise. A
|
|
47
|
+
# value a copy keeps from its original (KeepsCopiedValues) is the
|
|
48
|
+
# original's, as stored.
|
|
49
|
+
def refuse_nul_bytes
|
|
50
|
+
names = self.class.columns.filter_map do |column|
|
|
51
|
+
name = column.name
|
|
52
|
+
name if MEASURES.key?(column.type) && will_save_change_to_attribute?(name) && nul_byte?(name)
|
|
53
|
+
end
|
|
54
|
+
return if names.empty?
|
|
55
|
+
|
|
56
|
+
names.each { |name| errors.add(name, :nul_byte, message: ->(*) { I18n.t('permission_forge.validations.nul_byte') }) }
|
|
57
|
+
throw :abort
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def nul_byte?(name)
|
|
61
|
+
value = read_attribute(name)
|
|
62
|
+
value.is_a?(String) && value.include?("\0") && !(respond_to?(:copied_unchanged?, true) && copied_unchanged?(name))
|
|
63
|
+
end
|
|
64
|
+
|
|
41
65
|
def validate_column_lengths
|
|
42
66
|
self.class.columns.each do |column|
|
|
43
67
|
name = column.name
|
|
@@ -14,7 +14,7 @@ module PermissionForge
|
|
|
14
14
|
#
|
|
15
15
|
# resource = Resource.new(edit_access: true, read_access: false)
|
|
16
16
|
# resource.valid? # => false
|
|
17
|
-
# resource.errors[:edit_access] # => ["requires
|
|
17
|
+
# resource.errors[:edit_access] # => ["requires Read to be enabled"]
|
|
18
18
|
#
|
|
19
19
|
# @api private
|
|
20
20
|
module ValidatesDependency
|
|
@@ -44,7 +44,10 @@ module PermissionForge
|
|
|
44
44
|
define_method(method_name) do
|
|
45
45
|
return if belongs_to_muting_permission_set?
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
return unless send(field) && !send(requires)
|
|
48
|
+
|
|
49
|
+
label = I18n.t("permission_forge.validations.attributes.#{requires}", default: requires.to_s)
|
|
50
|
+
errors.add(field, :requires, message: I18n.t('permission_forge.validations.requires', requires: label))
|
|
48
51
|
end
|
|
49
52
|
private method_name
|
|
50
53
|
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
module PermissionForge
|
|
2
|
+
module Concerns
|
|
3
|
+
# Refuses nil for every column the schema declares NOT NULL as a
|
|
4
|
+
# validation error keyed by the attribute, so a JSON `null` or a blank
|
|
5
|
+
# form value (a boolean column casts "" to nil) is a 422 in the API and
|
|
6
|
+
# the form again in the admin, where the database raised NotNullViolation
|
|
7
|
+
# (a 500). Read from the schema: a NOT NULL column added later is covered
|
|
8
|
+
# without a change here.
|
|
9
|
+
#
|
|
10
|
+
# Left to whoever fills them: the primary key; on a new record the
|
|
11
|
+
# columns Rails or the gem fill when the row is created (the timestamps,
|
|
12
|
+
# `uuid` from HasUuid); a column that already has an error once every
|
|
13
|
+
# other validation ran — a presence rule's, with or without a condition
|
|
14
|
+
# — or whose required `belongs_to` names it as the foreign key (one
|
|
15
|
+
# message, not two). The check runs after every other validation of the
|
|
16
|
+
# model whatever the order they were declared in: an `:after` callback
|
|
17
|
+
# of the `validate` chain (canary `:validate_after_callback`).
|
|
18
|
+
#
|
|
19
|
+
# @api private
|
|
20
|
+
module ValidatesRequiredColumns
|
|
21
|
+
extend ActiveSupport::Concern
|
|
22
|
+
|
|
23
|
+
# Filled by the gem when a row is created (HasUuid, before_create).
|
|
24
|
+
FILLED_ON_CREATE = %w[uuid].freeze
|
|
25
|
+
# Filled by Rails when a row is created while record_timestamps is on.
|
|
26
|
+
TIMESTAMPS = %w[created_at created_on updated_at updated_on].freeze
|
|
27
|
+
|
|
28
|
+
included do
|
|
29
|
+
# After every `validate` / `validates` of the model, also those
|
|
30
|
+
# declared below the include or in a subclass.
|
|
31
|
+
set_callback :validate, :after, :validate_required_columns
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Whether `association` is a required belongs_to: Rails' own presence
|
|
35
|
+
# rule (message :required), whose condition holds whenever the
|
|
36
|
+
# foreign key is nil.
|
|
37
|
+
def self.required_association?(klass, association)
|
|
38
|
+
klass.validators_on(association).any? { |validator| validator.kind == :presence && validator.options[:message] == :required }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def validate_required_columns
|
|
44
|
+
self.class.columns.each do |column|
|
|
45
|
+
name = column.name
|
|
46
|
+
next if column.null || !has_attribute?(name) || !read_attribute(name).nil? || filled_elsewhere?(name)
|
|
47
|
+
|
|
48
|
+
errors.add(name, :blank, message: ->(*) { I18n.t('permission_forge.validations.blank') })
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Said already (an error on the attribute), or filled or checked by someone else.
|
|
53
|
+
def filled_elsewhere?(name)
|
|
54
|
+
klass = self.class
|
|
55
|
+
return true if errors.include?(name) || name == klass.primary_key
|
|
56
|
+
return true if new_record? && filled_on_create?(name)
|
|
57
|
+
|
|
58
|
+
required_foreign_key?(name)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def filled_on_create?(name)
|
|
62
|
+
FILLED_ON_CREATE.include?(name) || (self.class.record_timestamps && TIMESTAMPS.include?(name))
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# The foreign key of a required belongs_to: its error is keyed by the association.
|
|
66
|
+
def required_foreign_key?(name)
|
|
67
|
+
klass = self.class
|
|
68
|
+
klass.reflect_on_all_associations(:belongs_to).any? do |reflection|
|
|
69
|
+
reflection.foreign_key.to_s == name && ValidatesRequiredColumns.required_association?(klass, reflection.name)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -58,7 +58,8 @@ module PermissionForge
|
|
|
58
58
|
# - a context with an IP or session flags: those whose conditions pass
|
|
59
59
|
# (Services::PermissionSetConditionEvaluator), granting and muting alike;
|
|
60
60
|
# - no context at all — the resolver returned nothing (no resolver, as
|
|
61
|
-
# shipped): every conditional set, as before
|
|
61
|
+
# shipped): every conditional set, as before, but a granting set whose
|
|
62
|
+
# condition nobody can judge (ActivationConditions#never_grants?), which never applies;
|
|
62
63
|
# - under config.conditional_sets_strict_mode, no request context — the
|
|
63
64
|
# resolver returned nothing, or neither an IP nor session flags (a job,
|
|
64
65
|
# the console; the time alone is not a request context): only the
|
|
@@ -104,6 +105,8 @@ module PermissionForge
|
|
|
104
105
|
#
|
|
105
106
|
# @api private
|
|
106
107
|
Member = Data.define(:id, :muting, :requires_session_flag, :allowed_ips, :allowed_hours_start, :allowed_hours_end) do
|
|
108
|
+
include PermissionForge::ActivationConditions
|
|
109
|
+
|
|
107
110
|
def self.of(permission_set)
|
|
108
111
|
new(id: permission_set.id, muting: permission_set.is_muting == true,
|
|
109
112
|
requires_session_flag: permission_set.requires_session_flag, allowed_ips: permission_set.allowed_ips,
|
|
@@ -121,13 +124,17 @@ module PermissionForge
|
|
|
121
124
|
|
|
122
125
|
def muting? = muting
|
|
123
126
|
|
|
124
|
-
# Whether the set counts in `context`: its conditions pass — or
|
|
125
|
-
#
|
|
126
|
-
#
|
|
127
|
+
# Whether the set counts in `context`: its conditions pass — or it is
|
|
128
|
+
# a muting set whose condition cannot be judged: one nobody can judge
|
|
129
|
+
# (ActivationConditions#conditions_broken?: an IP entry that is not an
|
|
130
|
+
# address, one hour of the two, the same hour twice) in every mode, or
|
|
131
|
+
# one the context cannot answer (no IP for an IP allowlist, no flags
|
|
132
|
+
# at all for a session flag) in strict mode. A lockdown that never
|
|
133
|
+
# locked is the defect; a granting set never passes on such a condition.
|
|
127
134
|
def applies?(context, strict)
|
|
128
135
|
evaluator = Services::PermissionSetConditionEvaluator
|
|
129
136
|
evaluator.call(permission_set: self, context: context) ||
|
|
130
|
-
(
|
|
137
|
+
(muting && (conditions_broken? || (strict && evaluator.unjudgeable?(permission_set: self, context: context))))
|
|
131
138
|
end
|
|
132
139
|
end
|
|
133
140
|
|
|
@@ -353,7 +360,7 @@ module PermissionForge
|
|
|
353
360
|
klass = self.class
|
|
354
361
|
strict = klass.strict?
|
|
355
362
|
return sets.select(&:muting?) if strict && klass.no_context?(context)
|
|
356
|
-
return sets if context.blank?
|
|
363
|
+
return sets.reject(&:never_grants?) if context.blank?
|
|
357
364
|
|
|
358
365
|
sets.select { |member| member.applies?(context, strict) }
|
|
359
366
|
end
|
|
@@ -6,6 +6,10 @@ module PermissionForge
|
|
|
6
6
|
#
|
|
7
7
|
# @api public
|
|
8
8
|
module AdminUiSettings
|
|
9
|
+
# The default of admin_stale_assignment_days, and the threshold used
|
|
10
|
+
# wherever that setting is nil (the one definition of the fallback).
|
|
11
|
+
DEFAULT_STALE_ASSIGNMENT_DAYS = 90
|
|
12
|
+
|
|
9
13
|
# Lambda returning the model classes offered in "Add a resource".
|
|
10
14
|
# nil (default) eager-loads app/models and lists every concrete
|
|
11
15
|
# ActiveRecord model with a table, excluding PermissionForge's own.
|
|
@@ -19,7 +23,9 @@ module PermissionForge
|
|
|
19
23
|
attr_accessor :admin_session_flags
|
|
20
24
|
|
|
21
25
|
# Assignments not reviewed for this many days appear under
|
|
22
|
-
# "Stale assignments" on the admin home page
|
|
26
|
+
# "Stale assignments" on the admin home page; also the default threshold
|
|
27
|
+
# of StaleAssignmentAuditor and rake permission_forge:stale_assignments. Default: 90
|
|
28
|
+
# (DEFAULT_STALE_ASSIGNMENT_DAYS, also used when it is set to nil).
|
|
23
29
|
attr_accessor :admin_stale_assignment_days
|
|
24
30
|
|
|
25
31
|
# Name of the host app shown under the logo (e.g. "Acme CRM").
|
|
@@ -53,17 +59,24 @@ module PermissionForge
|
|
|
53
59
|
# or in middleware.
|
|
54
60
|
attr_accessor :admin_tenantless_access
|
|
55
61
|
|
|
62
|
+
# Whether an API-only application (`config.api_only = true`) that
|
|
63
|
+
# mounts the engine is warned that the admin screens need the cookies,
|
|
64
|
+
# session and flash middlewares (default true). false for an app that
|
|
65
|
+
# uses only the JSON API and never opens the admin screens.
|
|
66
|
+
attr_accessor :api_only_admin_warning
|
|
67
|
+
|
|
56
68
|
# Sets this group's defaults; called from Configuration#initialize.
|
|
57
69
|
# @return [void]
|
|
58
70
|
# @api private
|
|
59
71
|
def set_admin_ui_defaults
|
|
60
72
|
@admin_model_resolver = nil
|
|
61
73
|
@admin_session_flags = {}
|
|
62
|
-
@admin_stale_assignment_days =
|
|
74
|
+
@admin_stale_assignment_days = DEFAULT_STALE_ASSIGNMENT_DAYS
|
|
63
75
|
@admin_app_name = nil
|
|
64
76
|
@admin_back_url = nil
|
|
65
77
|
@admin_docs_url = 'https://github.com/fetijashari/permission_forge/blob/main/docs/configuration.md'
|
|
66
78
|
@admin_tenantless_access = :deny
|
|
79
|
+
@api_only_admin_warning = true
|
|
67
80
|
end
|
|
68
81
|
|
|
69
82
|
# @return [Boolean] whether an admin or admin API request may run with
|
|
@@ -86,7 +99,8 @@ module PermissionForge
|
|
|
86
99
|
names.select { |_, name| name.to_s.delete_prefix('::') == 'ActionController::Base' }.keys
|
|
87
100
|
end
|
|
88
101
|
|
|
89
|
-
# The boot warning for default_admin_controllers_under_tenancy, or nil
|
|
102
|
+
# The boot warning for default_admin_controllers_under_tenancy, or nil:
|
|
103
|
+
# the bare text (Warnings.emit puts the prefix and the severity on).
|
|
90
104
|
#
|
|
91
105
|
# @return [String, nil]
|
|
92
106
|
def tenantless_admin_warning
|
|
@@ -99,7 +113,7 @@ module PermissionForge
|
|
|
99
113
|
else
|
|
100
114
|
'Every admin request with no tenant is refused (admin_tenantless_access :deny).'
|
|
101
115
|
end
|
|
102
|
-
'
|
|
116
|
+
'tenant_model is set but ActionController::Base, which sets no ' \
|
|
103
117
|
"tenant, is still the base of #{names.join(' and ')}. #{consequence} Point each at a controller " \
|
|
104
118
|
'that sets the current tenant (or set the tenant in middleware).'
|
|
105
119
|
end
|
|
@@ -38,7 +38,8 @@ module PermissionForge
|
|
|
38
38
|
# user, an Audit::Actor, or a Hash { type:, id:, label: }. nil (default)
|
|
39
39
|
# falls back to current_user_resolver; under HTTP Basic auth the
|
|
40
40
|
# supplied username is recorded with actor_type 'basic_auth'.
|
|
41
|
-
#
|
|
41
|
+
# # Rails 8's authentication generator, under HTTP Basic (its sign-in does not run there):
|
|
42
|
+
# config.audit_actor_resolver = ->(controller) { Session.find_by(id: controller.request.cookie_jar.signed[:session_id])&.user }
|
|
42
43
|
attr_accessor :audit_actor_resolver
|
|
43
44
|
|
|
44
45
|
# Lambda(controller) returning the request context stored with each
|
|
@@ -36,9 +36,10 @@ module PermissionForge
|
|
|
36
36
|
|
|
37
37
|
# What the guard does with a resource or action no Permission Set manages:
|
|
38
38
|
#
|
|
39
|
-
# :
|
|
40
|
-
#
|
|
41
|
-
#
|
|
39
|
+
# :block — deny until a Permission Set permits it (default since 0.15)
|
|
40
|
+
# :allow — let it through: nothing is restricted until an admin adds the
|
|
41
|
+
# action to a Permission Set (the default before 0.15; an
|
|
42
|
+
# explicit opt-out that fails open, reported by SecurityAudit)
|
|
42
43
|
#
|
|
43
44
|
# A String is taken as its Symbol; any other value raises
|
|
44
45
|
# ConfigurationError (StrategyValues).
|
|
@@ -69,7 +70,7 @@ module PermissionForge
|
|
|
69
70
|
@controller_action_discovery = false
|
|
70
71
|
@controller_action_resolver = nil
|
|
71
72
|
@controller_discovery_exclude = DEFAULT_CONTROLLER_DISCOVERY_EXCLUDE.dup
|
|
72
|
-
@unconfigured_action_strategy = :
|
|
73
|
+
@unconfigured_action_strategy = :block
|
|
73
74
|
@action_guard_bypass = nil
|
|
74
75
|
@action_guard_nil_user_strategy = nil
|
|
75
76
|
end
|
|
@@ -6,6 +6,10 @@ module PermissionForge
|
|
|
6
6
|
#
|
|
7
7
|
# @api public
|
|
8
8
|
module MaskedAttributesSettings
|
|
9
|
+
# The masked_attributes_nil_user_strategy values that hide inside web
|
|
10
|
+
# requests.
|
|
11
|
+
HIDING_NIL_USER_STRATEGIES = %i[hide_in_requests hide].freeze
|
|
12
|
+
|
|
9
13
|
# Value returned by MaskedAttributes when read_access is denied. Default: nil.
|
|
10
14
|
attr_accessor :hidden_attribute_value
|
|
11
15
|
|
|
@@ -36,36 +40,86 @@ module PermissionForge
|
|
|
36
40
|
attr_accessor :masked_write_protection
|
|
37
41
|
|
|
38
42
|
# What the attribute guard does when there is no current user (a job, a
|
|
39
|
-
# mailer, the console, a resolver that returns nil).
|
|
40
|
-
# governs masking without a user: nil_user_strategy is
|
|
41
|
-
# response_filter_nil_user_strategy about
|
|
43
|
+
# mailer, the console, a public page, a resolver that returns nil).
|
|
44
|
+
# Nothing else governs masking without a user: nil_user_strategy is
|
|
45
|
+
# about rows, response_filter_nil_user_strategy about
|
|
46
|
+
# FiltersResponseAttributes.
|
|
47
|
+
#
|
|
48
|
+
# :bypass — every getter returns the stored value (the
|
|
49
|
+
# library default). Inside a web request each
|
|
50
|
+
# guarded attribute so returned is recorded:
|
|
51
|
+
# field.unmasked_without_user (runtime group
|
|
52
|
+
# :bypass, once per request per model and
|
|
53
|
+
# attribute) and one SECURITY warning per model
|
|
54
|
+
# and process in the log.
|
|
55
|
+
# :hide_in_requests — inside a web request (PermissionForge::WebRequest:
|
|
56
|
+
# a public page, a webhook, a controller that
|
|
57
|
+
# forgot authentication, the before_actions and
|
|
58
|
+
# middlewares that run before sign-in) every
|
|
59
|
+
# guarded attribute is hidden as under :hide;
|
|
60
|
+
# everywhere else (jobs, rake, runner, the
|
|
61
|
+
# console) as :bypass. What the generated
|
|
62
|
+
# initializer and config.preset = :strict set.
|
|
63
|
+
# :hide — every guarded attribute reads as
|
|
64
|
+
# hidden_attribute_value everywhere: getters,
|
|
65
|
+
# read_attribute, `[]`, attributes, as_json and
|
|
66
|
+
# the wrapped virtual readers and store accessors.
|
|
67
|
+
# Code that reads masked models with no user then
|
|
68
|
+
# sees hidden values: set the current user in
|
|
69
|
+
# those jobs and mailers.
|
|
42
70
|
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
# pluck) are untouched, as for a signed-in user. Code that
|
|
49
|
-
# reads masked models with no user then sees hidden values:
|
|
50
|
-
# set the current user in those jobs and mailers. The user
|
|
51
|
-
# model (user_class and its subclasses) is exempt: sign-in
|
|
52
|
-
# reads it before anyone is signed in (encrypted_password,
|
|
53
|
-
# confirmation_token, lockable counters).
|
|
71
|
+
# Where it hides, the primary key and the raw paths
|
|
72
|
+
# (read_attribute_before_type_cast, pluck) are untouched, as for a
|
|
73
|
+
# signed-in user, and the user model (user_class and its subclasses) is
|
|
74
|
+
# exempt: sign-in reads it before anyone is signed in
|
|
75
|
+
# (encrypted_password, confirmation_token, lockable counters).
|
|
54
76
|
attr_accessor :masked_attributes_nil_user_strategy
|
|
55
77
|
|
|
56
|
-
# @return [Boolean] whether the strategy is :hide
|
|
78
|
+
# @return [Boolean] whether the strategy is :hide: hidden everywhere
|
|
79
|
+
# (whatever the model; ask #hide_masked_attributes_without_user_for?
|
|
80
|
+
# for a record)
|
|
57
81
|
def hide_masked_attributes_without_user?
|
|
58
|
-
@masked_attributes_nil_user_strategy
|
|
82
|
+
@masked_attributes_nil_user_strategy == :hide
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# @return [Boolean] whether the strategy hides guarded attributes at
|
|
86
|
+
# least inside web requests (:hide_in_requests or :hide)
|
|
87
|
+
def hide_masked_attributes_without_user_in_requests?
|
|
88
|
+
HIDING_NIL_USER_STRATEGIES.include?(@masked_attributes_nil_user_strategy)
|
|
59
89
|
end
|
|
60
90
|
|
|
61
91
|
# The one decision of whether a record of klass hides its guarded
|
|
62
|
-
# attributes when nobody is signed in: under :hide,
|
|
63
|
-
#
|
|
92
|
+
# attributes when nobody is signed in: under :hide, and under
|
|
93
|
+
# :hide_in_requests inside a web request, every model but the user
|
|
94
|
+
# model.
|
|
64
95
|
#
|
|
65
96
|
# @param klass [Class]
|
|
66
97
|
# @return [Boolean]
|
|
67
98
|
def hide_masked_attributes_without_user_for?(klass)
|
|
68
|
-
|
|
99
|
+
masked_attributes_without_user_for(klass) == :hide
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# What the attribute guard does with a read of klass when nobody is
|
|
103
|
+
# signed in, the one definition behind #hide_masked_attributes_without_user_for?:
|
|
104
|
+
#
|
|
105
|
+
# :hide — hidden (:hide; :hide_in_requests inside a web request)
|
|
106
|
+
# :record — returned as stored and recorded (:bypass inside a web
|
|
107
|
+
# request: field.unmasked_without_user), the reads
|
|
108
|
+
# :hide_in_requests would hide
|
|
109
|
+
# :bypass — returned as stored (outside a web request, and always
|
|
110
|
+
# for the user model)
|
|
111
|
+
#
|
|
112
|
+
# Asked on every guarded read with no user: outside a web request
|
|
113
|
+
# under :bypass or :hide_in_requests it reads one execution-state slot.
|
|
114
|
+
#
|
|
115
|
+
# @param klass [Class]
|
|
116
|
+
# @return [Symbol] :hide, :record or :bypass
|
|
117
|
+
def masked_attributes_without_user_for(klass)
|
|
118
|
+
strategy = @masked_attributes_nil_user_strategy
|
|
119
|
+
return :bypass unless strategy == :hide || PermissionForge::WebRequest.active?
|
|
120
|
+
return :bypass if user_model_class?(klass)
|
|
121
|
+
|
|
122
|
+
strategy == :bypass ? :record : :hide
|
|
69
123
|
end
|
|
70
124
|
|
|
71
125
|
# Sets this group's defaults; called from Configuration#initialize.
|
|
@@ -6,7 +6,8 @@ module PermissionForge
|
|
|
6
6
|
# whether something fails open, applied as a layer of defaults under
|
|
7
7
|
# the settings the host makes itself.
|
|
8
8
|
#
|
|
9
|
-
# :compat (default) — the library defaults
|
|
9
|
+
# :compat (default) — the library defaults (Configuration.new; since 0.15
|
|
10
|
+
# unconfigured_action_strategy is :block there too)
|
|
10
11
|
# :strict — every fail-open default of the README's
|
|
11
12
|
# "Production hardening checklist" closed, and the
|
|
12
13
|
# settings that are already closed kept so
|
|
@@ -20,10 +21,10 @@ module PermissionForge
|
|
|
20
21
|
# The options that need host code to close (an authenticated
|
|
21
22
|
# base_controller_class, admin_assignment_authorization,
|
|
22
23
|
# audit_log_authorization, a request_context_resolver, user_active_check)
|
|
23
|
-
# are not in a preset.
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
24
|
+
# are not in a preset. :strict sets masked_attributes_nil_user_strategy
|
|
25
|
+
# to :hide_in_requests (hidden inside web requests with nobody signed in,
|
|
26
|
+
# stored values for jobs, rake and the console); :hide, which hides in
|
|
27
|
+
# jobs and mailers too, counts as closed but stays each app's choice.
|
|
27
28
|
#
|
|
28
29
|
# @api public
|
|
29
30
|
module Presets
|
|
@@ -34,36 +35,42 @@ module PermissionForge
|
|
|
34
35
|
nil_user_strategy: :bypass,
|
|
35
36
|
response_filter_nil_user_strategy: :bypass,
|
|
36
37
|
auto_query_scoping: :off,
|
|
37
|
-
|
|
38
|
+
no_read_access_strategy: :rules_only,
|
|
38
39
|
action_guard_nil_user_strategy: nil,
|
|
39
40
|
conditional_sets_strict_mode: false,
|
|
40
41
|
mask_virtual_attributes: false,
|
|
41
42
|
extra_fields_default_access: :grant,
|
|
42
43
|
cache_backend: :memory,
|
|
44
|
+
masked_attributes_nil_user_strategy: :bypass,
|
|
43
45
|
# closed by default
|
|
44
46
|
api_require_authorization: true,
|
|
45
47
|
admin_tenantless_access: :deny,
|
|
46
48
|
nested_attributes_parent_fallback: false,
|
|
47
|
-
unconfigured_field_strategy: :block
|
|
49
|
+
unconfigured_field_strategy: :block,
|
|
50
|
+
# closed by default since 0.15 (:allow is the explicit opt-out)
|
|
51
|
+
unconfigured_action_strategy: :block
|
|
48
52
|
}.freeze
|
|
49
53
|
|
|
50
54
|
STRICT = COMPAT.merge(
|
|
51
55
|
nil_user_strategy: :deny,
|
|
52
56
|
response_filter_nil_user_strategy: :block,
|
|
53
57
|
auto_query_scoping: :filter_only,
|
|
54
|
-
|
|
58
|
+
no_read_access_strategy: :deny,
|
|
55
59
|
action_guard_nil_user_strategy: :deny,
|
|
56
60
|
conditional_sets_strict_mode: true,
|
|
57
61
|
mask_virtual_attributes: :auto,
|
|
58
62
|
extra_fields_default_access: :unconfigured,
|
|
59
|
-
cache_backend: :rails_cache
|
|
63
|
+
cache_backend: :rails_cache,
|
|
64
|
+
masked_attributes_nil_user_strategy: :hide_in_requests
|
|
60
65
|
).freeze
|
|
61
66
|
|
|
62
67
|
PRESETS = { compat: COMPAT, strict: STRICT }.freeze
|
|
63
68
|
|
|
64
69
|
# Fail-open defaults of the hardening checklist that no preset closes:
|
|
65
|
-
# an explicit choice with a documented cost.
|
|
66
|
-
|
|
70
|
+
# an explicit choice with a documented cost. None at present
|
|
71
|
+
# (masked_attributes_nil_user_strategy left it in 0.15: :strict sets
|
|
72
|
+
# :hide_in_requests).
|
|
73
|
+
OPT_IN = [].freeze
|
|
67
74
|
OPTIONS = COMPAT.keys.freeze
|
|
68
75
|
|
|
69
76
|
# When an option counts as closed with a value other than the :strict
|
|
@@ -73,6 +80,8 @@ module PermissionForge
|
|
|
73
80
|
# nil follows nil_user_strategy
|
|
74
81
|
action_guard_nil_user_strategy: ->(_value, config) { !config.action_guard_nil_user_bypass? },
|
|
75
82
|
mask_virtual_attributes: ->(value, _config) { [true, 'auto'].include?(value) },
|
|
83
|
+
# hidden everywhere, jobs and the console included
|
|
84
|
+
masked_attributes_nil_user_strategy: ->(value, _config) { value == :hide },
|
|
76
85
|
# no cache at all, or a shared store the host built
|
|
77
86
|
cache_backend: ->(value, _config) { value == :null || value.is_a?(ActiveSupport::Cache::Store) }
|
|
78
87
|
}.freeze
|
|
@@ -18,6 +18,19 @@ module PermissionForge
|
|
|
18
18
|
module QueryScopingSettings
|
|
19
19
|
AUTO_QUERY_SCOPING_VALUES = %i[off filter_only].freeze
|
|
20
20
|
UNSCOPED_QUERY_DETECTION_VALUES = %i[off log raise].freeze
|
|
21
|
+
# Columns a record rule never compares (PermissionForge::SecretColumns),
|
|
22
|
+
# by name: password digests, tokens, `encrypted_*` columns (a gem's own
|
|
23
|
+
# encryption, attr_encrypted), secrets, then the credentials a
|
|
24
|
+
# user table commonly has — passwords and their salts, API / private /
|
|
25
|
+
# access keys, one-time-password seeds — anchored to name segments so
|
|
26
|
+
# `owner_id`, `sort_key`, `secretary` or `crypto_wallet` are never caught.
|
|
27
|
+
# An attribute declared with Active Record's `encrypts` keeps its column
|
|
28
|
+
# name: it is found by its declaration, not by a pattern
|
|
29
|
+
# (Operators::RuleProblem :encrypted_column).
|
|
30
|
+
DEFAULT_RULE_SECRET_COLUMN_PATTERNS = [
|
|
31
|
+
/\Apassword_digest\z/, /_digest\z/, /token/, /\Aencrypted_/, /_secret/, /passw/, /\Asecret(?:_|\z)/,
|
|
32
|
+
/(?:\A|_)(?:api|private|secret|access)_key\z/, /(?:\A|_)crypted(?:_|\z)/, /(?:\A|_)salt\z/, /(?:\A|_)otp(?:_|\z)/
|
|
33
|
+
].freeze
|
|
21
34
|
|
|
22
35
|
# Level 1 — ENFORCE.
|
|
23
36
|
# :off — (default) only find_by/find_by!/permission_scoped apply rules
|
|
@@ -33,6 +46,20 @@ module PermissionForge
|
|
|
33
46
|
# Unknown values raise ConfigurationError at assignment (see below).
|
|
34
47
|
attr_reader :auto_query_scoping
|
|
35
48
|
|
|
49
|
+
# What the record reads (find_by / find_by! / permission_scoped, and the
|
|
50
|
+
# default scope under auto_query_scoping) do for a signed-in user whose
|
|
51
|
+
# entry for the model's Resource chain does not grant read_access (no
|
|
52
|
+
# entry at all included):
|
|
53
|
+
# :rules_only — (default) apply the rules of the user's entries, if
|
|
54
|
+
# any: no rules, every row (can_read? is the check)
|
|
55
|
+
# :deny — no rows: permission_scoped and the default scope are
|
|
56
|
+
# empty, find_by / find_by! raise NotAuthorizedError for
|
|
57
|
+
# an existing record (nil / RecordNotFound for a missing
|
|
58
|
+
# one). The strict preset's value.
|
|
59
|
+
# With nobody signed in nil_user_strategy decides. One definition:
|
|
60
|
+
# PermissionForge::RecordReadAccess.
|
|
61
|
+
attr_accessor :no_read_access_strategy
|
|
62
|
+
|
|
36
63
|
# Level 2 — DETECT.
|
|
37
64
|
# :off — (default) no detection
|
|
38
65
|
# :log — Rails.logger.warn for each unscoped query on a protected table
|
|
@@ -59,6 +86,25 @@ module PermissionForge
|
|
|
59
86
|
# requires namespaced custom settings).
|
|
60
87
|
attr_accessor :rls_session_variable
|
|
61
88
|
|
|
89
|
+
# The column names a record rule may not compare (PermissionForge::
|
|
90
|
+
# SecretColumns, matched against the column name in lower case): a new
|
|
91
|
+
# rule on one, or a change of a rule's column or path to one, is
|
|
92
|
+
# refused, and the admin's rule builder does not offer them. Rules
|
|
93
|
+
# stored before keep evaluating; the audit and Home list them. `[]`
|
|
94
|
+
# opts out.
|
|
95
|
+
attr_reader :rule_secret_column_patterns
|
|
96
|
+
|
|
97
|
+
# @param patterns [Array<Regexp>]
|
|
98
|
+
# @raise [ConfigurationError] for anything that is not a list of regular expressions
|
|
99
|
+
def rule_secret_column_patterns=(patterns)
|
|
100
|
+
unless patterns.is_a?(Array) && patterns.all?(Regexp)
|
|
101
|
+
raise PermissionForge::ConfigurationError,
|
|
102
|
+
"Invalid rule_secret_column_patterns: #{patterns.inspect}. Give a list of regular expressions ([] opts out)."
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
@rule_secret_column_patterns = patterns
|
|
106
|
+
end
|
|
107
|
+
|
|
62
108
|
def auto_query_scoping=(value)
|
|
63
109
|
@auto_query_scoping = normalize_scoping_value(
|
|
64
110
|
value, AUTO_QUERY_SCOPING_VALUES, 'auto_query_scoping'
|
|
@@ -75,6 +121,13 @@ module PermissionForge
|
|
|
75
121
|
@auto_query_scoping == :filter_only
|
|
76
122
|
end
|
|
77
123
|
|
|
124
|
+
# @return [Boolean] whether the record reads of a signed-in user need
|
|
125
|
+
# read_access on the model's Resource chain. Only the exact
|
|
126
|
+
# :rules_only value applies the rules alone.
|
|
127
|
+
def deny_record_reads_without_read_access?
|
|
128
|
+
no_read_access_strategy.to_s != 'rules_only'
|
|
129
|
+
end
|
|
130
|
+
|
|
78
131
|
def unscoped_query_detection_active?
|
|
79
132
|
return false if @unscoped_query_detection == :off
|
|
80
133
|
return false unless defined?(Rails) && Rails.respond_to?(:env)
|
|
@@ -86,10 +139,12 @@ module PermissionForge
|
|
|
86
139
|
|
|
87
140
|
def set_query_scoping_defaults
|
|
88
141
|
@auto_query_scoping = :off
|
|
142
|
+
@no_read_access_strategy = :rules_only
|
|
89
143
|
@unscoped_query_detection = :off
|
|
90
144
|
@unscoped_query_detection_environments = %w[development test]
|
|
91
145
|
@database_row_level_security = false
|
|
92
146
|
@rls_session_variable = 'permission_forge.user_id'
|
|
147
|
+
@rule_secret_column_patterns = DEFAULT_RULE_SECRET_COLUMN_PATTERNS.dup
|
|
93
148
|
end
|
|
94
149
|
|
|
95
150
|
# Fail closed on typos: an unknown value must never silently enable a
|