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
|
@@ -6,8 +6,6 @@ module PermissionForge
|
|
|
6
6
|
# a read-only query; actions (extend, keep, clean up, acknowledge) live in
|
|
7
7
|
# the controllers.
|
|
8
8
|
class AttentionItems
|
|
9
|
-
ENDING_DAYS = 7
|
|
10
|
-
|
|
11
9
|
# `field` — the field row, when there is one, switched on or off.
|
|
12
10
|
# `implicit_grant` — no active field row, and the field is readable and
|
|
13
11
|
# editable anyway because the model declares it as an extra (virtual) field.
|
|
@@ -18,6 +16,16 @@ module PermissionForge
|
|
|
18
16
|
def turned_off? = !field.nil? && !field.is_active
|
|
19
17
|
def implicit_grant? = implicit_grant == true
|
|
20
18
|
def name = field&.name || column
|
|
19
|
+
|
|
20
|
+
# Masking, acknowledging or adding the field changes what the locked
|
|
21
|
+
# field, Resource or Permission Set holds: refused (EditableLock), so
|
|
22
|
+
# Home offers neither "Fix" nor "This is intended" for it.
|
|
23
|
+
def locked? = !EditableLock.locked_record(field, resource).nil?
|
|
24
|
+
|
|
25
|
+
# Home offers "This is intended" for it: a field that is shown unmasked
|
|
26
|
+
# and can be changed (acknowledging needs the column, see
|
|
27
|
+
# .acknowledgement_supported?).
|
|
28
|
+
def acknowledgeable? = !unconfigured? && !locked?
|
|
21
29
|
end
|
|
22
30
|
|
|
23
31
|
TidyFinding = Struct.new(:permission_set, :kind, :people_count, keyword_init: true) do
|
|
@@ -42,22 +50,75 @@ module PermissionForge
|
|
|
42
50
|
end
|
|
43
51
|
end
|
|
44
52
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
53
|
+
# An active rule in effect that matches nothing because it names a
|
|
54
|
+
# column or an association that is not there (Operators::RuleProblem):
|
|
55
|
+
# everyone it applies to is denied every record of its resource.
|
|
56
|
+
BrokenRuleFinding = Struct.new(:rule, :problem, :people_count, keyword_init: true) do
|
|
57
|
+
def resource = rule.resource
|
|
58
|
+
def permission_set = rule.resource.permission_set
|
|
59
|
+
|
|
60
|
+
# The rule, its Resource or its Permission Set is locked: Home opens
|
|
61
|
+
# the Rules tab instead of the rule's edit page (which refuses it).
|
|
62
|
+
def locked? = !EditableLock.locked_record(rule).nil?
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# A field row in effect masked with a strategy this app does not know.
|
|
66
|
+
MaskStrategyFinding = Struct.new(:field, :people_count, keyword_init: true) do
|
|
67
|
+
def resource = field.resource
|
|
68
|
+
def permission_set = field.resource.permission_set
|
|
49
69
|
end
|
|
50
70
|
|
|
51
|
-
|
|
71
|
+
# A set whose activation condition nobody can judge: a muting one
|
|
72
|
+
# applies to every request, a granting one never passes.
|
|
73
|
+
ConditionsFinding = Struct.new(:permission_set, :people_count, keyword_init: true)
|
|
74
|
+
|
|
75
|
+
# A rule in effect that compares a column holding a secret
|
|
76
|
+
# (PermissionForge::SecretColumns), stored before the model refused one.
|
|
77
|
+
SecretRuleFinding = Struct.new(:rule, :people_count, keyword_init: true) do
|
|
78
|
+
def resource = rule.resource
|
|
79
|
+
def permission_set = rule.resource.permission_set
|
|
80
|
+
def locked? = !EditableLock.locked_record(rule).nil?
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def self.acknowledgement_supported? = PermissionForge::ResourceField.acknowledgement_supported?
|
|
84
|
+
|
|
85
|
+
# The threshold of "due for review", in days: config.admin_stale_assignment_days,
|
|
86
|
+
# or the default while it is nil. The one definition — Home's Stale
|
|
87
|
+
# card, the People page (pf_stale?), the audit's stale_assignments and
|
|
88
|
+
# StaleAssignmentAuditor all read it.
|
|
89
|
+
#
|
|
90
|
+
# @param configuration [Configuration]
|
|
91
|
+
# @return [Integer]
|
|
92
|
+
def self.stale_days(configuration = PermissionForge.configuration)
|
|
93
|
+
configuration.try(:admin_stale_assignment_days) || Configuration::AdminUiSettings::DEFAULT_STALE_ASSIGNMENT_DAYS
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# The catalog only counts (`verify: false`): every admin page draws the
|
|
97
|
+
# navigation's count, so it reads the schema cache, never the database.
|
|
98
|
+
def initialize(configuration: PermissionForge.configuration, catalog: ModelCatalog.new(verify: false),
|
|
52
99
|
sensitive: SensitiveColumns.new(configuration: configuration))
|
|
53
100
|
@configuration = configuration
|
|
54
101
|
@catalog = catalog
|
|
55
102
|
@sensitive = sensitive
|
|
56
103
|
end
|
|
57
104
|
|
|
105
|
+
# Every list Home draws a card for (the navigation counts them all) =>
|
|
106
|
+
# the partial (home/_<partial>) that draws it, in Home's priority
|
|
107
|
+
# order: the one list Home renders from (#cards).
|
|
108
|
+
CARDS = { audit_findings: 'audit_card', broken_rules: 'broken_rules_card', privacy_findings: 'privacy_card',
|
|
109
|
+
unknown_mask_strategies: 'mask_strategies_card', broken_conditions: 'conditions_card',
|
|
110
|
+
secret_rules: 'secret_rules_card', unenforced_rules: 'rules_card', ending_soon: 'ending_card',
|
|
111
|
+
expired: 'expired_card', stale: 'stale_card', tidy: 'tidy_card' }.freeze
|
|
112
|
+
|
|
58
113
|
def total
|
|
59
|
-
|
|
60
|
-
|
|
114
|
+
CARDS.each_key.sum { |card| public_send(card).size }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# The partials of the cards Home draws: those whose list has items.
|
|
118
|
+
#
|
|
119
|
+
# @return [Array<String>]
|
|
120
|
+
def cards
|
|
121
|
+
CARDS.filter_map { |list, partial| partial if public_send(list).any? }
|
|
61
122
|
end
|
|
62
123
|
|
|
63
124
|
def all_good?
|
|
@@ -83,12 +144,64 @@ module PermissionForge
|
|
|
83
144
|
def unenforced_rules
|
|
84
145
|
@unenforced_rules ||= begin
|
|
85
146
|
enforced = rule_enforced_resources
|
|
86
|
-
counts =
|
|
147
|
+
counts = unenforced_rule_counts(enforced.keys)
|
|
87
148
|
findings = counts.map { |id, count| RulesFinding.build(enforced.fetch(id), count, people_counts) }
|
|
88
149
|
findings.sort_by { |finding| [finding.permission_set.name, finding.resource.name] }
|
|
89
150
|
end
|
|
90
151
|
end
|
|
91
152
|
|
|
153
|
+
# Rules in effect that name a column or an association that is not
|
|
154
|
+
# there — the audit's missing_rule_columns finding, the same rules.
|
|
155
|
+
def broken_rules
|
|
156
|
+
@broken_rules ||= rules_in_effect.filter_map do |rule|
|
|
157
|
+
problem = PermissionForge::Operators::RuleProblem.for(rule)
|
|
158
|
+
next unless problem
|
|
159
|
+
|
|
160
|
+
BrokenRuleFinding.new(rule: rule, problem: problem, people_count: people_counts.fetch(rule.resource.permission_set_id, 0))
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Rules in effect that compare a column holding a secret — the audit's
|
|
165
|
+
# secret_rule_columns finding, the same rules. They still apply.
|
|
166
|
+
def secret_rules
|
|
167
|
+
@secret_rules ||= begin
|
|
168
|
+
secrets = PermissionForge::SecretColumns.new(configuration: @configuration)
|
|
169
|
+
rules_in_effect.select { |rule| secrets.rule?(rule) }.map do |rule|
|
|
170
|
+
SecretRuleFinding.new(rule: rule, people_count: people_counts.fetch(rule.resource.permission_set_id, 0))
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# --- Field rows and conditions the engine cannot read as written ----------
|
|
176
|
+
|
|
177
|
+
# Field rows in effect masked with a strategy this app does not know
|
|
178
|
+
# (ResourceField.with_unknown_mask_strategy, the audit's
|
|
179
|
+
# unknown_mask_strategies): masked fully until fixed.
|
|
180
|
+
def unknown_mask_strategies
|
|
181
|
+
@unknown_mask_strategies ||= PermissionForge::ResourceField.with_unknown_mask_strategy.map do |field|
|
|
182
|
+
MaskStrategyFinding.new(field: field, people_count: people_counts.fetch(field.resource.permission_set_id, 0))
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Sets whose activation conditions nobody can judge
|
|
187
|
+
# (PermissionSet.with_broken_conditions, the audit's incomplete_conditions).
|
|
188
|
+
def broken_conditions
|
|
189
|
+
@broken_conditions ||= PermissionForge::PermissionSet.with_broken_conditions.map do |set|
|
|
190
|
+
ConditionsFinding.new(permission_set: set, people_count: people_counts.fetch(set.id, 0))
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Active rules of the granting resources in effect: the rules that are
|
|
195
|
+
# applied right now (a muting set's rules never are). Not the ones View
|
|
196
|
+
# All skips — nobody is denied by those.
|
|
197
|
+
def rules_in_effect
|
|
198
|
+
@rules_in_effect ||= begin
|
|
199
|
+
enforced = granting_resources.reject(&:rules_bypassed?).map(&:id)
|
|
200
|
+
PermissionForge::PermissionSetRule.active.where(resource_id: enforced)
|
|
201
|
+
.includes(resource: :permission_set).order(:resource_id, :id).to_a
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
92
205
|
def auto_query_scoping?
|
|
93
206
|
@configuration.auto_query_scoping_enabled?
|
|
94
207
|
end
|
|
@@ -96,8 +209,7 @@ module PermissionForge
|
|
|
96
209
|
# --- Assignments --------------------------------------------------------
|
|
97
210
|
|
|
98
211
|
def ending_soon
|
|
99
|
-
@ending_soon ||= assignments.
|
|
100
|
-
.order(:expires_at).to_a
|
|
212
|
+
@ending_soon ||= assignments.ending_within.order(:expires_at).to_a
|
|
101
213
|
end
|
|
102
214
|
|
|
103
215
|
def expired
|
|
@@ -110,7 +222,7 @@ module PermissionForge
|
|
|
110
222
|
end
|
|
111
223
|
|
|
112
224
|
def stale_days
|
|
113
|
-
@configuration
|
|
225
|
+
self.class.stale_days(@configuration)
|
|
114
226
|
end
|
|
115
227
|
|
|
116
228
|
# --- Permission Sets to tidy up -----------------------------------------
|
|
@@ -129,42 +241,51 @@ module PermissionForge
|
|
|
129
241
|
PermissionForge::PermissionSetUser.includes(:permission_set, :user)
|
|
130
242
|
end
|
|
131
243
|
|
|
132
|
-
# Resources whose rules are in force (View All / Modify All
|
|
244
|
+
# Readable Resources whose rules are in force (View All / Modify All
|
|
245
|
+
# skip them), by id. A Resource named after no model this app can load
|
|
246
|
+
# (`Custmer`) has no query to scope: the audit's integrity/unknown_models names it.
|
|
133
247
|
def rule_enforced_resources
|
|
134
|
-
|
|
248
|
+
readable_resources.reject { |resource| resource.rules_bypassed? || @catalog.find(resource.name).nil? }
|
|
249
|
+
.index_by(&:id)
|
|
135
250
|
end
|
|
136
251
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
252
|
+
# Active rules the app's query scoping does not apply, counted per
|
|
253
|
+
# resource: every one while auto_query_scoping is off, the join rules
|
|
254
|
+
# (PermissionSetRule#join_rule?, the one definition) when it is on.
|
|
255
|
+
def unenforced_rule_counts(resource_ids)
|
|
256
|
+
rows = PermissionForge::PermissionSetRule.active.where(resource_id: resource_ids)
|
|
257
|
+
rows = rows.join_type_rules if auto_query_scoping?
|
|
258
|
+
rows.pluck(:resource_id, :join_type)
|
|
259
|
+
.select { |_, join_type| !auto_query_scoping? || !PermissionForge::PermissionSetRule.filter_join_type?(join_type) }
|
|
260
|
+
.group_by(&:first).transform_values(&:size)
|
|
140
261
|
end
|
|
141
262
|
|
|
142
263
|
def people_counts
|
|
143
264
|
@people_counts ||= PermissionForge::PermissionSetUser.active_assignments.group(:permission_set_id).count
|
|
144
265
|
end
|
|
145
266
|
|
|
267
|
+
# The granting Resource rows that count right now (Resource.in_effect,
|
|
268
|
+
# the one definition), with their sets and fields loaded.
|
|
146
269
|
def granting_resources
|
|
147
|
-
@granting_resources ||= PermissionForge::Resource.
|
|
148
|
-
.where(permission_set_id: PermissionForge::PermissionSet.active.granting.select(:id))
|
|
270
|
+
@granting_resources ||= PermissionForge::Resource.in_effect.merge(PermissionForge::PermissionSet.granting)
|
|
149
271
|
.includes(:permission_set, :resource_fields).to_a
|
|
150
272
|
end
|
|
151
273
|
|
|
274
|
+
# Those of them people can read: where an unmasked sensitive field shows.
|
|
275
|
+
def readable_resources
|
|
276
|
+
@readable_resources ||= granting_resources.select(&:read_access)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Readable and unmasked, and not acknowledged as intentional
|
|
280
|
+
# (ResourceField#exposes_unacknowledged?).
|
|
152
281
|
def unmasked_sensitive_fields
|
|
153
|
-
|
|
154
|
-
granting_resources.flat_map do |resource|
|
|
282
|
+
readable_resources.flat_map do |resource|
|
|
155
283
|
resource.resource_fields.filter_map do |field|
|
|
156
|
-
sensitive_finding(resource, field.name, field: field) if
|
|
284
|
+
sensitive_finding(resource, field.name, field: field) if field.exposes_unacknowledged?
|
|
157
285
|
end
|
|
158
286
|
end
|
|
159
287
|
end
|
|
160
288
|
|
|
161
|
-
# Readable and unmasked, and not acknowledged as intentional.
|
|
162
|
-
def exposed?(field, acknowledged)
|
|
163
|
-
return false unless field.is_active && field.read_access && !field.mask_access
|
|
164
|
-
|
|
165
|
-
!(acknowledged && field.sensitive_acknowledged_at.present?)
|
|
166
|
-
end
|
|
167
|
-
|
|
168
289
|
# Sensitive columns and virtual fields with no active field row are a
|
|
169
290
|
# leak when they are readable unmasked anyway: the unconfigured-field
|
|
170
291
|
# strategy lets them through, or the model declares them as extra fields
|
|
@@ -175,7 +296,7 @@ module PermissionForge
|
|
|
175
296
|
passes = %w[pass_through warn].include?(@configuration.unconfigured_field_strategy.to_s)
|
|
176
297
|
return [] unless passes || @configuration.grant_extra_fields_by_default?
|
|
177
298
|
|
|
178
|
-
|
|
299
|
+
readable_resources.flat_map { |resource| unconfigured_findings(resource, passes) }
|
|
179
300
|
end
|
|
180
301
|
|
|
181
302
|
# `passes` — whether unconfigured_field_strategy lets a field with no row through.
|
|
@@ -37,9 +37,7 @@ module PermissionForge
|
|
|
37
37
|
# log can be read while recording is switched off; without the table
|
|
38
38
|
# there is nothing to read and every list is empty.
|
|
39
39
|
def self.table?
|
|
40
|
-
PermissionForge::
|
|
41
|
-
rescue StandardError
|
|
42
|
-
false
|
|
40
|
+
PermissionForge::DatabaseReach.table_exists?(PermissionForge::AuditEvent)
|
|
43
41
|
end
|
|
44
42
|
|
|
45
43
|
# Why the log is not recording, or nil when it records:
|
|
@@ -134,12 +132,11 @@ module PermissionForge
|
|
|
134
132
|
Page.new(events: events, total: total, number: current, per_page: per_page)
|
|
135
133
|
end
|
|
136
134
|
|
|
137
|
-
# 1 for anything that is not a number
|
|
135
|
+
# 1 for anything that is not a whole number from 1 (ParamShapes.integer,
|
|
136
|
+
# the one reader); at most the last page.
|
|
138
137
|
def self.page_number(number, total, per_page)
|
|
139
|
-
return 1 unless number.to_s.match?(/\A\d{1,9}\z/)
|
|
140
|
-
|
|
141
138
|
last = [(total + per_page - 1) / per_page, 1].max
|
|
142
|
-
number.
|
|
139
|
+
ParamShapes.integer(number, default: 1, range: 1..).clamp(1, [last, MAX_PAGE].min)
|
|
143
140
|
end
|
|
144
141
|
|
|
145
142
|
# People the log names lately (KNOWN_PEOPLE_WINDOW), for the actor and
|
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
module PermissionForge
|
|
4
4
|
module Admin
|
|
5
|
-
# Who created and who last edited each permission record.
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# HTTP Basic login or an API client
|
|
11
|
-
#
|
|
5
|
+
# Who created and who last edited each permission record. The creator is
|
|
6
|
+
# the actor of the record's creation event when the audit log has one
|
|
7
|
+
# (LastChanges.creations), else created_by; the last edit is the record's
|
|
8
|
+
# latest change event when the log has one (LastChanges), else
|
|
9
|
+
# updated_by / updated_at. An event names who really acted, while the
|
|
10
|
+
# columns stamp nobody for an HTTP Basic login or an API client (and
|
|
11
|
+
# updated_by keeps the previous editor). The full history is the audit
|
|
12
|
+
# log (Admin::AuditLog).
|
|
12
13
|
class AuditTrail
|
|
13
14
|
TYPES = {
|
|
14
15
|
'permission_set' => 'PermissionForge::PermissionSet',
|
|
@@ -22,11 +23,13 @@ module PermissionForge
|
|
|
22
23
|
}.freeze
|
|
23
24
|
|
|
24
25
|
DAY_FILTERS = [7, 30, 90].freeze
|
|
25
|
-
|
|
26
|
+
# The CSV's columns, named by permission_forge.ui.audit.csv.<column>.
|
|
27
|
+
CSV_COLUMNS = %w[type record where created_by created_at edited_by edited_at].freeze
|
|
26
28
|
|
|
27
29
|
# `last_event` — the record's latest change event, or nil (no events:
|
|
28
30
|
# auditing off, or the record predates the audit log).
|
|
29
|
-
|
|
31
|
+
# `creation_event` — the record's creation event, or nil (likewise).
|
|
32
|
+
Entry = Struct.new(:type, :record, :last_event, :creation_event, keyword_init: true) do
|
|
30
33
|
def created_by = record.try(:created_by)
|
|
31
34
|
def updated_by = record.try(:updated_by)
|
|
32
35
|
def created_at = record.created_at
|
|
@@ -42,6 +45,13 @@ module PermissionForge
|
|
|
42
45
|
|
|
43
46
|
last_event.actor_id if last_event.actor_type == 'user'
|
|
44
47
|
end
|
|
48
|
+
|
|
49
|
+
# The creator's user id as a String, or nil (not a person, or unknown).
|
|
50
|
+
def creator_id
|
|
51
|
+
return created_by_id&.to_s unless creation_event
|
|
52
|
+
|
|
53
|
+
creation_event.actor_id if creation_event.actor_type == 'user'
|
|
54
|
+
end
|
|
45
55
|
end
|
|
46
56
|
|
|
47
57
|
def self.type_of(record)
|
|
@@ -73,7 +83,7 @@ module PermissionForge
|
|
|
73
83
|
|
|
74
84
|
# Every record belonging to a Permission Set (the set, its resources,
|
|
75
85
|
# fields, actions, rules and assignments), filtered and sorted.
|
|
76
|
-
# Filters (all optional): who:, type:, days
|
|
86
|
+
# Filters (all optional): who:, type:, days: (an Integer of DAY_FILTERS), sort: ('asc' / 'desc', default 'desc').
|
|
77
87
|
# @return [Array<Entry>]
|
|
78
88
|
def for_permission_set(permission_set, **filters)
|
|
79
89
|
entries = apply_filters(permission_set_entries(permission_set), filters)
|
|
@@ -84,7 +94,8 @@ module PermissionForge
|
|
|
84
94
|
# @param describe [#call] (entry) -> [type_label, record_label, where]
|
|
85
95
|
# @param person [#call] (entry) -> [created by, last edited by]
|
|
86
96
|
def to_csv(entries, describe:, person:)
|
|
87
|
-
|
|
97
|
+
header = CSV_COLUMNS.map { |column| I18n.t("permission_forge.ui.audit.csv.#{column}") }
|
|
98
|
+
Csv.document([header] + entries.map { |entry| csv_row(entry, describe, person) })
|
|
88
99
|
end
|
|
89
100
|
|
|
90
101
|
private
|
|
@@ -109,14 +120,14 @@ module PermissionForge
|
|
|
109
120
|
|
|
110
121
|
# Created it, or made its last edit (by the event when there is one).
|
|
111
122
|
def made_by?(entry, user_id)
|
|
112
|
-
entry.
|
|
123
|
+
entry.creator_id == user_id.to_s || entry.editor_id == user_id.to_s
|
|
113
124
|
end
|
|
114
125
|
|
|
115
126
|
def apply_filters(entries, filters)
|
|
116
127
|
type, who, days = filters.values_at(:type, :who, :days)
|
|
117
128
|
entries = entries.select { |e| e.type == type } if type.present? && TYPES.key?(type)
|
|
118
129
|
entries = filter_who(entries, who) if who.present?
|
|
119
|
-
entries = filter_days(entries, days
|
|
130
|
+
entries = filter_days(entries, days) if DAY_FILTERS.include?(days)
|
|
120
131
|
entries
|
|
121
132
|
end
|
|
122
133
|
|
|
@@ -147,14 +158,19 @@ module PermissionForge
|
|
|
147
158
|
with_people(scope).map { |record| [type, record] }
|
|
148
159
|
end
|
|
149
160
|
|
|
150
|
-
# [[type, record], ...] → entries, each with its latest change event
|
|
161
|
+
# [[type, record], ...] → entries, each with its latest change event
|
|
162
|
+
# and its creation event.
|
|
151
163
|
def entries_for(typed_records)
|
|
152
|
-
|
|
153
|
-
|
|
164
|
+
records = typed_records.map(&:last)
|
|
165
|
+
last = LastChanges.new(records)
|
|
166
|
+
created = LastChanges.creations(records)
|
|
167
|
+
typed_records.map do |type, record|
|
|
168
|
+
Entry.new(type: type, record: record, last_event: last.last_edit(record), creation_event: created[record])
|
|
169
|
+
end
|
|
154
170
|
end
|
|
155
171
|
|
|
156
172
|
def filter_who(entries, who)
|
|
157
|
-
entries.select { |e| e.
|
|
173
|
+
entries.select { |e| e.creator_id == who.to_s || e.editor_id == who.to_s }
|
|
158
174
|
end
|
|
159
175
|
|
|
160
176
|
def filter_days(entries, days)
|
|
@@ -4,7 +4,10 @@ module PermissionForge
|
|
|
4
4
|
module Admin
|
|
5
5
|
# Resolves pasted emails/names (one per line or comma-separated) to people
|
|
6
6
|
# for bulk assignment. Exact, case-insensitive matches only — a near miss
|
|
7
|
-
# is reported as "not found" rather than guessed.
|
|
7
|
+
# is reported as "not found" rather than guessed. A column the user model
|
|
8
|
+
# encrypts (PermissionForge::EncryptedAttributes) holds ciphertext: a
|
|
9
|
+
# deterministically encrypted one matches the pasted value exactly
|
|
10
|
+
# (`where(email: value)` encrypts it), any other is not matched. Only people within
|
|
8
11
|
# config.user_list_resolver match (PeopleDirectory#find_all): anyone else
|
|
9
12
|
# is "not found" too.
|
|
10
13
|
class BulkPeopleMatcher
|
|
@@ -60,14 +63,28 @@ module PermissionForge
|
|
|
60
63
|
end
|
|
61
64
|
|
|
62
65
|
def lookup(token)
|
|
63
|
-
|
|
64
|
-
return exact(column_if_present('email'), needle) if token.include?('@')
|
|
66
|
+
return exact(email_columns, token) if token.include?('@')
|
|
65
67
|
|
|
66
|
-
exact(name_column,
|
|
68
|
+
exact(name_column, token) || first_last_match(token)
|
|
67
69
|
end
|
|
68
70
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
+
# The first listed person whose value in any of `columns` equals the token.
|
|
72
|
+
def exact(columns, token)
|
|
73
|
+
scopes = Array(columns).filter_map { |column| equal_to(column, token) }
|
|
74
|
+
first_listed(scopes.reduce(:or)) if scopes.any?
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# The people whose `column` equals the token, whatever its case; for a
|
|
78
|
+
# deterministically encrypted column the value as pasted or lower-cased
|
|
79
|
+
# (the attribute type encrypts it); nil for a column SQL cannot compare
|
|
80
|
+
# (encrypted otherwise).
|
|
81
|
+
def equal_to(column, token)
|
|
82
|
+
if PermissionForge::EncryptedAttributes.encrypted?(model, column)
|
|
83
|
+
return unless PermissionForge::EncryptedAttributes.deterministic?(model, column)
|
|
84
|
+
|
|
85
|
+
return model.where(column => [token, token.downcase].uniq)
|
|
86
|
+
end
|
|
87
|
+
model.where(model.arel_table[column].lower.eq(token.downcase))
|
|
71
88
|
end
|
|
72
89
|
|
|
73
90
|
# The first match that config.user_list holds.
|
|
@@ -79,26 +96,27 @@ module PermissionForge
|
|
|
79
96
|
@directory ||= PeopleDirectory.new(configuration: @configuration)
|
|
80
97
|
end
|
|
81
98
|
|
|
82
|
-
|
|
83
|
-
|
|
99
|
+
# Every email column the user table has (`email`, `email_address`).
|
|
100
|
+
def email_columns
|
|
101
|
+
PermissionForge::SchemaRead.columns_among(model, PersonLabel::EMAIL_ATTRIBUTES.map(&:to_s))
|
|
84
102
|
end
|
|
85
103
|
|
|
86
104
|
# "Ana Krasniqi" against separate first/last name columns.
|
|
87
|
-
def first_last_match(
|
|
88
|
-
first, last =
|
|
105
|
+
def first_last_match(token)
|
|
106
|
+
first, last = token.split(/\s+/, 2)
|
|
89
107
|
pair = first_last_columns
|
|
90
108
|
return nil unless pair && last
|
|
91
109
|
|
|
92
|
-
|
|
93
|
-
first_listed(
|
|
110
|
+
scopes = [equal_to(pair.first, first), equal_to(pair.last, last)]
|
|
111
|
+
first_listed(scopes.first.merge(scopes.last)) if scopes.all?
|
|
94
112
|
end
|
|
95
113
|
|
|
96
114
|
def first_last_columns
|
|
97
|
-
FIRST_LAST_COLUMNS.find { |cols| (cols
|
|
115
|
+
FIRST_LAST_COLUMNS.find { |cols| PermissionForge::SchemaRead.columns_among(model, cols).size == cols.size }
|
|
98
116
|
end
|
|
99
117
|
|
|
100
118
|
def name_column
|
|
101
|
-
(%w[name full_name display_name]
|
|
119
|
+
PermissionForge::SchemaRead.columns_among(model, %w[name full_name display_name]).first
|
|
102
120
|
end
|
|
103
121
|
|
|
104
122
|
def picked(ids)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
# Whether a Permission Set holds more than one Resource of a
|
|
6
|
+
# single-table-inheritance chain (ResourceChain: a base and a subclass on
|
|
7
|
+
# its table) — then a row that names the set about one of them says which
|
|
8
|
+
# ("Sales rep · VipCustomer", LabelsHelper#pf_set_entry_name): the field
|
|
9
|
+
# drawer's "Across all Permission Sets" and a person's effective access
|
|
10
|
+
# showed two rows "Sales rep" with different levels and no way to tell
|
|
11
|
+
# them apart (field test round 7, B#35). Every Resource row of the set
|
|
12
|
+
# counts, turned off or not: it is about naming, not about access.
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# ChainEntries.several?(set, %w[VipCustomer Customer]) # => true when the set has both Resources
|
|
16
|
+
#
|
|
17
|
+
# @api private
|
|
18
|
+
module ChainEntries
|
|
19
|
+
# @param permission_set [PermissionSet]
|
|
20
|
+
# @param names [Array<String, Symbol>] the Resource names of the chain
|
|
21
|
+
# @return [Boolean]
|
|
22
|
+
def self.several?(permission_set, names)
|
|
23
|
+
names = names.map(&:to_s)
|
|
24
|
+
return false if names.size < 2
|
|
25
|
+
|
|
26
|
+
permission_set.resources.count { |resource| names.include?(resource.name) } > 1
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# The Resource rows named by one of `names`, in the order of `names`
|
|
30
|
+
# (a chain: the model first, then its bases nearest first), so the row
|
|
31
|
+
# nearest the model comes first (EffectiveAccess: the Resource a muting
|
|
32
|
+
# set is named with, B#38).
|
|
33
|
+
#
|
|
34
|
+
# @param resources [Array<Resource>] one Permission Set's rows
|
|
35
|
+
# @param names [Array<String>]
|
|
36
|
+
# @return [Array<Resource>]
|
|
37
|
+
def self.in_chain_order(resources, names)
|
|
38
|
+
names.flat_map { |name| resources.select { |resource| resource.name == name } }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -52,12 +52,16 @@ module PermissionForge
|
|
|
52
52
|
end
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
+
# A flag the request turned on that stays off because what it needs is
|
|
56
|
+
# off (`needs`: :read, or :view_all_and_edit for Modify All). Not a change.
|
|
57
|
+
Refusal = Struct.new(:resource, :attribute, :needs, keyword_init: true)
|
|
58
|
+
|
|
55
59
|
class Invalid < StandardError
|
|
56
60
|
attr_reader :record
|
|
57
61
|
|
|
58
62
|
def initialize(record)
|
|
59
63
|
@record = record
|
|
60
|
-
super(record.errors
|
|
64
|
+
super(ListSentence.call(ErrorSentence.all(record.errors)))
|
|
61
65
|
end
|
|
62
66
|
end
|
|
63
67
|
|
|
@@ -79,6 +83,13 @@ module PermissionForge
|
|
|
79
83
|
changes.empty?
|
|
80
84
|
end
|
|
81
85
|
|
|
86
|
+
# @return [Array<Refusal>] what the request asked for and the
|
|
87
|
+
# dependencies kept off (Edit with Read off, ...)
|
|
88
|
+
def refused
|
|
89
|
+
changes
|
|
90
|
+
@context.refused
|
|
91
|
+
end
|
|
92
|
+
|
|
82
93
|
# Saves every pending record, all or nothing, under one audit
|
|
83
94
|
# correlation id. Its own savepoint (requires_new) rolls back the saves
|
|
84
95
|
# before a failing one — and their audit events — also when the caller's
|
|
@@ -13,7 +13,7 @@ module PermissionForge
|
|
|
13
13
|
|
|
14
14
|
private
|
|
15
15
|
|
|
16
|
-
delegate :permission_set, :params, :sensitive, :muting?, :section, :resources, :resource_of,
|
|
16
|
+
delegate :permission_set, :params, :sensitive, :muting?, :section, :resources, :resource_of, :refused,
|
|
17
17
|
to: :@context
|
|
18
18
|
|
|
19
19
|
def change(**attrs)
|
|
@@ -43,10 +43,11 @@ module PermissionForge
|
|
|
43
43
|
section('bulk')
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
-
#
|
|
47
|
-
#
|
|
46
|
+
# Locked records (is_editable: false on them or on what they belong
|
|
47
|
+
# to: EditableLock, the one definition) are managed by the system/API
|
|
48
|
+
# and never changed from the admin UI (their disabled inputs still post).
|
|
48
49
|
def locked?(record)
|
|
49
|
-
|
|
50
|
+
EditableLock.locked?(record)
|
|
50
51
|
end
|
|
51
52
|
|
|
52
53
|
def truthy?(value)
|
|
@@ -8,7 +8,8 @@ module PermissionForge
|
|
|
8
8
|
# same resource/field objects, so later parts see earlier parts' edits
|
|
9
9
|
# (e.g. field levels see the resource's new Edit flag).
|
|
10
10
|
class Context
|
|
11
|
-
|
|
11
|
+
# refused: the ChangeSet::Refusal list the parts add to.
|
|
12
|
+
attr_reader :permission_set, :params, :sensitive, :refused
|
|
12
13
|
|
|
13
14
|
def initialize(permission_set:, params:, sensitive:, catalog: nil)
|
|
14
15
|
@permission_set = permission_set
|
|
@@ -16,6 +17,7 @@ module PermissionForge
|
|
|
16
17
|
@sensitive = sensitive
|
|
17
18
|
@controller_catalog = catalog
|
|
18
19
|
@muting = permission_set.is_muting
|
|
20
|
+
@refused = []
|
|
19
21
|
end
|
|
20
22
|
|
|
21
23
|
# The app's controller actions (see NewActions). Built on first use:
|
|
@@ -9,8 +9,11 @@ module PermissionForge
|
|
|
9
9
|
class FieldEdit
|
|
10
10
|
ATTRS = %w[label description is_active].freeze
|
|
11
11
|
MASK_LEVELS = %w[masked mask].freeze
|
|
12
|
+
# A blank label over a generated one is no change (Prefill.posted);
|
|
13
|
+
# over a label an admin wrote it is what the model stores for a blank
|
|
14
|
+
# label, the name (ResourceField#label_to_store: NormalizesLabel).
|
|
12
15
|
CASTS = {
|
|
13
|
-
'label' => ->(value, field) {
|
|
16
|
+
'label' => ->(value, field) { field.label_to_store(Prefill.posted(field, :label, value).to_s.strip) },
|
|
14
17
|
'is_active' => ->(value, _field) { Values.truthy?(value) }
|
|
15
18
|
}.freeze
|
|
16
19
|
TEXT_CAST = ->(value, _field) { value.to_s.strip.presence }
|
|
@@ -37,7 +37,7 @@ module PermissionForge
|
|
|
37
37
|
return [] unless resource
|
|
38
38
|
|
|
39
39
|
fields = Array(bulk['keys']).filter_map { |key| resource.resource_fields.find { |field| field.name == key.to_s } }
|
|
40
|
-
fields.reject { |field| field
|
|
40
|
+
fields.reject { |field| locked?(field) }
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def changes_for_field(field, attrs)
|
|
@@ -80,7 +80,7 @@ module PermissionForge
|
|
|
80
80
|
end
|
|
81
81
|
|
|
82
82
|
def build_new_action(resource, key, permitted)
|
|
83
|
-
action = resource.resource_actions.build(name: key, label: key
|
|
83
|
+
action = resource.resource_actions.build(name: key, label: key, is_active: true,
|
|
84
84
|
is_permitted: truthy?(permitted))
|
|
85
85
|
change(kind: :new_action, record: action, attribute: 'is_permitted', from: nil, to: action.is_permitted,
|
|
86
86
|
resource: resource)
|