permission_forge 0.14.1 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +2372 -0
- data/README.md +76 -25
- data/app/controllers/concerns/permission_forge/api/crud_actions.rb +3 -2
- data/app/controllers/concerns/permission_forge/api/input_shapes.rb +200 -0
- data/app/controllers/concerns/permission_forge/api/pagination.rb +12 -5
- data/app/controllers/concerns/permission_forge/api/response_renderer.rb +37 -1
- data/app/controllers/concerns/permission_forge/api_authentication.rb +13 -2
- data/app/controllers/concerns/permission_forge/cache_confirmation.rb +42 -11
- data/app/controllers/concerns/permission_forge/change_review.rb +42 -0
- data/app/controllers/concerns/permission_forge/copy_refusal.rb +39 -0
- data/app/controllers/concerns/permission_forge/param_shapes.rb +207 -7
- data/app/controllers/concerns/permission_forge/save_notice.rb +69 -0
- data/app/controllers/permission_forge/api/audit_events_controller.rb +1 -1
- data/app/controllers/permission_forge/api/base_controller.rb +163 -12
- data/app/controllers/permission_forge/api/dashboard_controller.rb +29 -4
- data/app/controllers/permission_forge/api/permission_set_group_users_controller.rb +5 -2
- data/app/controllers/permission_forge/api/permission_set_groups_controller.rb +5 -4
- data/app/controllers/permission_forge/api/permission_set_rules_controller.rb +7 -5
- data/app/controllers/permission_forge/api/permission_set_users_controller.rb +80 -19
- data/app/controllers/permission_forge/api/permission_sets_controller.rb +12 -6
- data/app/controllers/permission_forge/api/resource_actions_controller.rb +1 -1
- data/app/controllers/permission_forge/api/resource_fields_controller.rb +3 -4
- data/app/controllers/permission_forge/api/resources_controller.rb +26 -12
- data/app/controllers/permission_forge/api/unmatched_routes_controller.rb +20 -0
- data/app/controllers/permission_forge/application_controller.rb +39 -5
- data/app/controllers/permission_forge/assignments_controller.rb +36 -34
- data/app/controllers/permission_forge/bulk_assignments_controller.rb +8 -4
- data/app/controllers/permission_forge/field_lookups_controller.rb +1 -1
- data/app/controllers/permission_forge/maintenance_controller.rb +6 -2
- data/app/controllers/permission_forge/people_controller.rb +14 -14
- data/app/controllers/permission_forge/permission_set_editor.rb +20 -6
- data/app/controllers/permission_forge/permission_set_group_users_controller.rb +3 -1
- data/app/controllers/permission_forge/permission_set_groups_controller.rb +23 -9
- data/app/controllers/permission_forge/permission_set_rules_controller.rb +20 -10
- data/app/controllers/permission_forge/permission_sets_controller.rb +23 -24
- data/app/controllers/permission_forge/resource_actions_controller.rb +11 -6
- data/app/controllers/permission_forge/resource_fields_controller.rb +37 -19
- data/app/controllers/permission_forge/resources_controller.rb +12 -11
- data/app/frontend/permission_forge/pf_admin.css +99 -17
- data/app/frontend/permission_forge/pf_admin.js +36 -0
- data/app/helpers/permission_forge/application_helper.rb +22 -72
- data/app/helpers/permission_forge/audit_attribution_helper.rb +77 -0
- data/app/helpers/permission_forge/audit_helper.rb +57 -119
- data/app/helpers/permission_forge/audit_records_helper.rb +103 -0
- data/app/helpers/permission_forge/audit_sentences_helper.rb +56 -2
- data/app/helpers/permission_forge/badges_helper.rb +74 -17
- data/app/helpers/permission_forge/changes_helper.rb +24 -6
- data/app/helpers/permission_forge/conditions_helper.rb +119 -0
- data/app/helpers/permission_forge/labels_helper.rb +134 -0
- data/app/helpers/permission_forge/people_helper.rb +11 -19
- data/app/helpers/permission_forge/rule_problems_helper.rb +73 -0
- data/app/helpers/permission_forge/sentences_helper.rb +119 -98
- data/app/helpers/permission_forge/settings_helper.rb +2 -2
- data/app/helpers/permission_forge/ui_helper.rb +65 -4
- data/app/models/permission_forge/audit_event.rb +5 -1
- data/app/models/permission_forge/permission_set.rb +208 -18
- data/app/models/permission_forge/permission_set_group.rb +10 -4
- data/app/models/permission_forge/permission_set_group_user.rb +3 -1
- data/app/models/permission_forge/permission_set_rule.rb +254 -17
- data/app/models/permission_forge/permission_set_user.rb +38 -3
- data/app/models/permission_forge/resource.rb +18 -8
- data/app/models/permission_forge/resource_action.rb +15 -6
- data/app/models/permission_forge/resource_field.rb +97 -11
- data/app/views/permission_forge/assignments/confirm_destroy.html.erb +4 -4
- data/app/views/permission_forge/assignments/new.html.erb +10 -6
- data/app/views/permission_forge/audit_events/index.html.erb +1 -1
- data/app/views/permission_forge/audit_events/show.html.erb +16 -9
- data/app/views/permission_forge/bulk_assignments/confirm_revoke.html.erb +3 -3
- data/app/views/permission_forge/bulk_assignments/new.html.erb +3 -3
- data/app/views/permission_forge/field_lookups/show.html.erb +2 -2
- data/app/views/permission_forge/home/_broken_rules_card.html.erb +35 -0
- data/app/views/permission_forge/home/_conditions_card.html.erb +28 -0
- data/app/views/permission_forge/home/_ending_card.html.erb +11 -5
- data/app/views/permission_forge/home/_expired_card.html.erb +1 -1
- data/app/views/permission_forge/home/_mask_strategies_card.html.erb +27 -0
- data/app/views/permission_forge/home/_privacy_card.html.erb +21 -7
- data/app/views/permission_forge/home/_rules_card.html.erb +1 -1
- data/app/views/permission_forge/home/_secret_rules_card.html.erb +33 -0
- data/app/views/permission_forge/home/_stale_card.html.erb +7 -5
- data/app/views/permission_forge/home/_tidy_card.html.erb +1 -1
- data/app/views/permission_forge/home/index.html.erb +5 -6
- data/app/views/permission_forge/people/_access_tab.html.erb +26 -19
- data/app/views/permission_forge/people/_audit_tab.html.erb +8 -8
- data/app/views/permission_forge/people/_effective_field_source.html.erb +13 -7
- data/app/views/permission_forge/people/_effective_tab.html.erb +16 -15
- data/app/views/permission_forge/people/_effective_why.html.erb +20 -16
- data/app/views/permission_forge/people/_why_item.html.erb +1 -1
- data/app/views/permission_forge/people/confirm_remove_all.html.erb +3 -3
- data/app/views/permission_forge/people/erasure_preview.html.erb +1 -1
- data/app/views/permission_forge/permission_set_groups/confirm_destroy.html.erb +3 -3
- data/app/views/permission_forge/permission_set_groups/index.html.erb +3 -3
- data/app/views/permission_forge/permission_set_groups/show.html.erb +28 -19
- data/app/views/permission_forge/permission_set_rules/_builder.html.erb +35 -15
- data/app/views/permission_forge/permission_set_rules/_preview.html.erb +17 -0
- data/app/views/permission_forge/permission_set_rules/confirm_destroy.html.erb +3 -3
- data/app/views/permission_forge/permission_set_rules/preview.html.erb +12 -0
- data/app/views/permission_forge/permission_sets/_assignments_tab.html.erb +37 -22
- data/app/views/permission_forge/permission_sets/_audit_tab.html.erb +4 -4
- data/app/views/permission_forge/permission_sets/_condition_problems.html.erb +15 -0
- data/app/views/permission_forge/permission_sets/_conditions_tab.html.erb +14 -8
- data/app/views/permission_forge/permission_sets/_header.html.erb +6 -5
- data/app/views/permission_forge/permission_sets/_records_summary.html.erb +2 -2
- data/app/views/permission_forge/permission_sets/_resources_tab.html.erb +4 -4
- data/app/views/permission_forge/permission_sets/confirm_destroy.html.erb +4 -4
- data/app/views/permission_forge/permission_sets/confirm_switch_mode.html.erb +3 -3
- data/app/views/permission_forge/permission_sets/index.html.erb +6 -6
- data/app/views/permission_forge/permission_sets/new.html.erb +2 -2
- data/app/views/permission_forge/permission_sets/review.html.erb +13 -3
- data/app/views/permission_forge/permission_sets/show.html.erb +7 -5
- data/app/views/permission_forge/resource_actions/_across_sets.html.erb +7 -10
- data/app/views/permission_forge/resource_actions/_effect.html.erb +5 -5
- data/app/views/permission_forge/resource_actions/_form.html.erb +1 -1
- data/app/views/permission_forge/resource_actions/_managed_note.html.erb +5 -3
- data/app/views/permission_forge/resource_actions/confirm_destroy.html.erb +1 -1
- data/app/views/permission_forge/resource_actions/new.html.erb +1 -1
- data/app/views/permission_forge/resource_actions/show.html.erb +2 -2
- data/app/views/permission_forge/resource_fields/_explain.html.erb +19 -15
- data/app/views/permission_forge/resource_fields/show.html.erb +21 -12
- data/app/views/permission_forge/resources/_actions_tab.html.erb +4 -4
- data/app/views/permission_forge/resources/_fields_tab.html.erb +17 -9
- data/app/views/permission_forge/resources/_rules_tab.html.erb +9 -6
- data/app/views/permission_forge/resources/_sync.html.erb +8 -5
- data/app/views/permission_forge/resources/confirm_destroy.html.erb +3 -3
- data/app/views/permission_forge/resources/new.html.erb +3 -2
- data/app/views/permission_forge/resources/show.html.erb +6 -6
- data/app/views/permission_forge/settings/_row.html.erb +2 -2
- data/app/views/permission_forge/settings/show.html.erb +10 -1
- data/app/views/permission_forge/shared/_across_sets.html.erb +8 -9
- data/app/views/permission_forge/shared/_audit_events.html.erb +2 -2
- data/app/views/permission_forge/shared/_audit_footer.html.erb +13 -2
- data/app/views/permission_forge/shared/_crud_cell.html.erb +6 -6
- data/app/views/permission_forge/shared/_mode_banners.html.erb +1 -1
- data/app/views/permission_forge/shared/_savebar.html.erb +4 -2
- data/config/locales/permission_forge.en.yml +480 -69
- data/config/locales/permission_forge.mk.yml +499 -79
- data/config/locales/permission_forge.sq.yml +509 -80
- data/config/routes.rb +38 -23
- data/lib/generators/permission_forge/install/install_generator.rb +21 -0
- data/lib/generators/permission_forge/install/templates/initializer.rb.tt +35 -34
- data/lib/generators/permission_forge/install/templates/migration.rb.tt +5 -5
- data/lib/permission_forge/activation_conditions.rb +200 -0
- data/lib/permission_forge/admin/action_label.rb +33 -0
- data/lib/permission_forge/admin/action_rows.rb +3 -1
- data/lib/permission_forge/admin/actor_label.rb +52 -0
- data/lib/permission_forge/admin/attention_items.rb +152 -31
- data/lib/permission_forge/admin/audit_log.rb +4 -7
- data/lib/permission_forge/admin/audit_trail.rb +33 -17
- data/lib/permission_forge/admin/bulk_people_matcher.rb +32 -14
- data/lib/permission_forge/admin/chain_entries.rb +42 -0
- data/lib/permission_forge/admin/change_set.rb +12 -1
- data/lib/permission_forge/admin/change_set_parts/base.rb +5 -4
- data/lib/permission_forge/admin/change_set_parts/context.rb +3 -1
- data/lib/permission_forge/admin/change_set_parts/field_edit.rb +4 -1
- data/lib/permission_forge/admin/change_set_parts/field_levels.rb +1 -1
- data/lib/permission_forge/admin/change_set_parts/new_actions.rb +1 -1
- data/lib/permission_forge/admin/change_set_parts/new_fields.rb +38 -11
- data/lib/permission_forge/admin/change_set_parts/resource_edit.rb +17 -1
- data/lib/permission_forge/admin/change_set_parts/resource_flags.rb +1 -1
- data/lib/permission_forge/admin/change_set_parts/set_attrs.rb +4 -0
- data/lib/permission_forge/admin/change_set_parts/values.rb +10 -4
- data/lib/permission_forge/admin/column_fields.rb +22 -21
- data/lib/permission_forge/admin/conditions_form.rb +47 -15
- data/lib/permission_forge/admin/controller_catalog.rb +42 -14
- data/lib/permission_forge/admin/editable_lock.rb +83 -3
- data/lib/permission_forge/admin/effective_access.rb +147 -48
- data/lib/permission_forge/admin/effective_fields.rb +75 -67
- data/lib/permission_forge/admin/error_sentence.rb +47 -0
- data/lib/permission_forge/admin/expiry_presets.rb +17 -0
- data/lib/permission_forge/admin/exports/holdings.rb +6 -3
- data/lib/permission_forge/admin/field_across_sets.rb +7 -4
- data/lib/permission_forge/admin/field_chain.rb +109 -0
- data/lib/permission_forge/admin/field_detail.rb +24 -11
- data/lib/permission_forge/admin/field_label.rb +109 -0
- data/lib/permission_forge/admin/field_level.rb +4 -68
- data/lib/permission_forge/admin/field_provenance.rb +132 -0
- data/lib/permission_forge/admin/field_rows.rb +26 -11
- data/lib/permission_forge/admin/last_changes.rb +14 -0
- data/lib/permission_forge/admin/list_sentence.rb +32 -0
- data/lib/permission_forge/admin/mask_strategy_choices.rb +57 -0
- data/lib/permission_forge/admin/model_catalog.rb +83 -39
- data/lib/permission_forge/admin/model_label.rb +66 -0
- data/lib/permission_forge/admin/people_candidates.rb +21 -9
- data/lib/permission_forge/admin/people_directory.rb +23 -5
- data/lib/permission_forge/admin/permission_set_factory.rb +6 -15
- data/lib/permission_forge/admin/person_label.rb +105 -16
- data/lib/permission_forge/admin/prefill.rb +92 -0
- data/lib/permission_forge/admin/record_name.rb +45 -0
- data/lib/permission_forge/admin/rule_builder.rb +77 -3
- data/lib/permission_forge/admin/rule_form.rb +44 -10
- data/lib/permission_forge/admin/rule_form_input.rb +27 -11
- data/lib/permission_forge/admin/rule_notes.rb +43 -0
- data/lib/permission_forge/admin/rule_preview.rb +96 -0
- data/lib/permission_forge/admin/sensitive_columns.rb +10 -62
- data/lib/permission_forge/admin/settings_catalog.rb +64 -12
- data/lib/permission_forge/admin/state_label.rb +97 -0
- data/lib/permission_forge/admin_method_override.rb +44 -0
- data/lib/permission_forge/api/schema.rb +322 -0
- data/lib/permission_forge/api/serializers/base_serializer.rb +87 -1
- data/lib/permission_forge/api/serializers/permission_set_group_user_serializer.rb +1 -1
- data/lib/permission_forge/api/serializers/permission_set_rule_serializer.rb +1 -1
- data/lib/permission_forge/api/serializers/permission_set_serializer.rb +5 -2
- data/lib/permission_forge/api/serializers/permission_set_user_serializer.rb +1 -1
- data/lib/permission_forge/api/serializers/resource_action_serializer.rb +1 -1
- data/lib/permission_forge/api/serializers/resource_field_serializer.rb +1 -1
- data/lib/permission_forge/api/serializers/resource_serializer.rb +1 -1
- data/lib/permission_forge/api_authorizations.rb +59 -0
- data/lib/permission_forge/api_only_admin.rb +81 -0
- data/lib/permission_forge/audit/actor_codes.rb +32 -0
- data/lib/permission_forge/audit/actor_resolver.rb +1 -2
- data/lib/permission_forge/audit/boot_warnings.rb +29 -9
- data/lib/permission_forge/audit/event_catalog.rb +16 -6
- data/lib/permission_forge/audit/event_query.rb +3 -3
- data/lib/permission_forge/audit/reason_codes.rb +25 -0
- data/lib/permission_forge/audit/runtime.rb +9 -2
- data/lib/permission_forge/audit/settings_observer.rb +2 -2
- data/lib/permission_forge/audit/unmasked_without_user.rb +169 -0
- data/lib/permission_forge/audit.rb +24 -10
- data/lib/permission_forge/boot_checks.rb +180 -63
- data/lib/permission_forge/cache/base.rb +10 -0
- data/lib/permission_forge/cache/null_cache.rb +5 -0
- data/lib/permission_forge/cache/outcome.rb +119 -0
- data/lib/permission_forge/cache/rails_cache.rb +6 -0
- data/lib/permission_forge/cache/report.rb +150 -0
- data/lib/permission_forge/cache/user_keys.rb +8 -4
- data/lib/permission_forge/cache_warnings.rb +140 -0
- data/lib/permission_forge/concerns/auto_permission_scoping.rb +19 -2
- data/lib/permission_forge/concerns/has_tenant_scoping.rb +1 -1
- data/lib/permission_forge/concerns/has_uuid.rb +1 -1
- data/lib/permission_forge/concerns/keeps_copied_values.rb +48 -0
- data/lib/permission_forge/concerns/masked_attributes/alias_access.rb +12 -2
- data/lib/permission_forge/concerns/masked_attributes/virtual_readers.rb +6 -4
- data/lib/permission_forge/concerns/masked_attributes.rb +150 -34
- data/lib/permission_forge/concerns/normalizes_label.rb +78 -0
- data/lib/permission_forge/concerns/permission_cache_accessor.rb +3 -3
- data/lib/permission_forge/concerns/permission_scoped_findable.rb +121 -29
- data/lib/permission_forge/concerns/permissioned_user.rb +1 -1
- data/lib/permission_forge/concerns/records_audit_events.rb +13 -6
- data/lib/permission_forge/concerns/requires_name.rb +37 -0
- data/lib/permission_forge/concerns/validates_column_lengths.rb +24 -0
- data/lib/permission_forge/concerns/validates_dependency.rb +5 -2
- data/lib/permission_forge/concerns/validates_required_columns.rb +74 -0
- data/lib/permission_forge/conditional_sets.rb +13 -6
- data/lib/permission_forge/configuration/admin_ui_settings.rb +18 -4
- data/lib/permission_forge/configuration/audit_settings.rb +2 -1
- data/lib/permission_forge/configuration/controller_action_settings.rb +5 -4
- data/lib/permission_forge/configuration/masked_attributes_settings.rb +73 -19
- data/lib/permission_forge/configuration/presets.rb +20 -11
- data/lib/permission_forge/configuration/query_scoping_settings.rb +55 -0
- data/lib/permission_forge/configuration/strategy_values.rb +2 -1
- data/lib/permission_forge/configuration.rb +41 -18
- data/lib/permission_forge/controller_actions/managed_index.rb +8 -7
- data/lib/permission_forge/database_reach.rb +93 -0
- data/lib/permission_forge/deprecation.rb +1 -1
- data/lib/permission_forge/docs/api_samples.rb +250 -0
- data/lib/permission_forge/docs/configuration_reference.rb +172 -0
- data/lib/permission_forge/encrypted_attributes.rb +150 -0
- data/lib/permission_forge/engine.rb +46 -14
- data/lib/permission_forge/execution_state/restore.rb +74 -0
- data/lib/permission_forge/execution_state.rb +8 -1
- data/lib/permission_forge/field_level.rb +123 -0
- data/lib/permission_forge/field_names.rb +145 -0
- data/lib/permission_forge/field_variants.rb +96 -0
- data/lib/permission_forge/generated_text.rb +95 -0
- data/lib/permission_forge/inspection_filters.rb +83 -0
- data/lib/permission_forge/managed_fields.rb +66 -0
- data/lib/permission_forge/masked_models.rb +25 -19
- data/lib/permission_forge/masking_sync.rb +100 -25
- data/lib/permission_forge/model_bindings.rb +30 -19
- data/lib/permission_forge/model_lookup.rb +42 -0
- data/lib/permission_forge/operators/encrypted_alias.rb +67 -0
- data/lib/permission_forge/operators/registry.rb +6 -0
- data/lib/permission_forge/operators/rule_column.rb +242 -27
- data/lib/permission_forge/operators/rule_path.rb +179 -0
- data/lib/permission_forge/operators/rule_problem.rb +309 -0
- data/lib/permission_forge/operators/rule_sources.rb +96 -0
- data/lib/permission_forge/operators/value_casters/postgresql_types.rb +237 -0
- data/lib/permission_forge/operators/value_casters/temporal.rb +9 -20
- data/lib/permission_forge/operators/value_casters.rb +61 -13
- data/lib/permission_forge/operators/value_typing.rb +83 -7
- data/lib/permission_forge/query_scoping_warnings.rb +22 -17
- data/lib/permission_forge/rake_audit.rb +5 -4
- data/lib/permission_forge/rake_dry_run.rb +32 -0
- data/lib/permission_forge/rake_output.rb +189 -0
- data/lib/permission_forge/record_read_access.rb +51 -0
- data/lib/permission_forge/resource_chain.rb +14 -2
- data/lib/permission_forge/schema_read/ordered_types.rb +72 -0
- data/lib/permission_forge/schema_read.rb +335 -0
- data/lib/permission_forge/secret_columns.rb +49 -0
- data/lib/permission_forge/sensitive_columns.rb +114 -0
- data/lib/permission_forge/services/attribute_access_guard.rb +22 -14
- data/lib/permission_forge/services/bulk_permission_set_assignment.rb +16 -8
- data/lib/permission_forge/services/concerns/clonable.rb +9 -0
- data/lib/permission_forge/services/concerns/settles_cache.rb +58 -0
- data/lib/permission_forge/services/consent_withdrawal_handler.rb +37 -19
- data/lib/permission_forge/services/data_subject_erasure.rb +83 -45
- data/lib/permission_forge/services/expired_assignment_cleaner.rb +62 -25
- data/lib/permission_forge/services/extra_field_defaults.rb +1 -1
- data/lib/permission_forge/services/field_permission_merger.rb +44 -17
- data/lib/permission_forge/services/filters_response_attributes.rb +20 -5
- data/lib/permission_forge/services/gdpr_field_auditor/text_report.rb +33 -5
- data/lib/permission_forge/services/gdpr_field_auditor.rb +68 -48
- data/lib/permission_forge/services/host_key_guard.rb +8 -5
- data/lib/permission_forge/services/mask_value.rb +72 -11
- data/lib/permission_forge/services/permission_cache_clearer.rb +36 -15
- data/lib/permission_forge/services/permission_hash_builder.rb +48 -19
- data/lib/permission_forge/services/permission_rule_applier.rb +131 -68
- data/lib/permission_forge/services/permission_set_cloner.rb +32 -8
- data/lib/permission_forge/services/permission_set_condition_evaluator.rb +38 -36
- data/lib/permission_forge/services/permission_set_group_cloner.rb +2 -1
- data/lib/permission_forge/services/permits_attributes/result.rb +31 -0
- data/lib/permission_forge/services/permits_attributes.rb +48 -27
- data/lib/permission_forge/services/resource_field_generator/row_builder.rb +96 -0
- data/lib/permission_forge/services/resource_field_generator.rb +71 -42
- data/lib/permission_forge/services/resource_with_fields_builder.rb +2 -1
- data/lib/permission_forge/services/rls_operator_sql.rb +83 -24
- data/lib/permission_forge/services/rls_policy_generator.rb +83 -39
- data/lib/permission_forge/services/rls_read_access_sql.rb +131 -0
- data/lib/permission_forge/services/security_audit/action_check.rb +49 -5
- data/lib/permission_forge/services/security_audit/assignment_check.rb +2 -1
- data/lib/permission_forge/services/security_audit/audit_trail_check.rb +1 -3
- data/lib/permission_forge/services/security_audit/conditions_check.rb +92 -0
- data/lib/permission_forge/services/security_audit/config_check.rb +41 -18
- data/lib/permission_forge/services/security_audit/integrity_check.rb +71 -4
- data/lib/permission_forge/services/security_audit/masking_without_user.rb +66 -0
- data/lib/permission_forge/services/security_audit/personal_data_check.rb +7 -11
- data/lib/permission_forge/services/security_audit/rule_check.rb +180 -50
- data/lib/permission_forge/services/security_audit/text_report.rb +14 -2
- data/lib/permission_forge/services/security_audit/unmanaged_actions.rb +145 -0
- data/lib/permission_forge/services/security_audit/user_model_check.rb +4 -3
- data/lib/permission_forge/services/security_audit.rb +16 -4
- data/lib/permission_forge/services/stale_assignment_auditor.rb +8 -5
- data/lib/permission_forge/timestamp_fields.rb +60 -0
- data/lib/permission_forge/unique_conflict.rb +117 -0
- data/lib/permission_forge/version.rb +1 -1
- data/lib/permission_forge/virtual_fields/discovery.rb +2 -1
- data/lib/permission_forge/virtual_fields.rb +15 -5
- data/lib/permission_forge/warnings.rb +263 -0
- data/lib/permission_forge/web_request.rb +248 -0
- data/lib/permission_forge.rb +54 -0
- data/lib/tasks/permission_forge.rake +26 -6
- data/lib/tasks/permission_forge_audit_events.rake +8 -4
- data/lib/tasks/permission_forge_cache.rake +12 -12
- data/lib/tasks/permission_forge_docs.rake +48 -0
- data/lib/tasks/permission_forge_expiration.rake +11 -5
- data/lib/tasks/permission_forge_gdpr.rake +36 -20
- metadata +84 -1
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
# The field entry tables a resource's fields are composed from, its own
|
|
6
|
+
# first, then those of the single-table-inheritance bases the person has
|
|
7
|
+
# an entry for — composed as ResourceChain.compose_field composes them
|
|
8
|
+
# for the attribute guard: one entry decides alone; among several, one
|
|
9
|
+
# that does not grant read hides every configured field, else the
|
|
10
|
+
# strictest field entry decides (the first in chain order at its level;
|
|
11
|
+
# at Hidden or Read-only, one no mute brought there first). A muting set
|
|
12
|
+
# is credited with hiding the field, or taking its editing away, only
|
|
13
|
+
# when the granting sets alone, composed the same way, would show it,
|
|
14
|
+
# or let it be edited (review R4 P1-2).
|
|
15
|
+
# Admin::EffectiveFields builds it; FieldProvenance words the entry that
|
|
16
|
+
# decides.
|
|
17
|
+
#
|
|
18
|
+
# @example
|
|
19
|
+
# chain = FieldChain.new([FieldChain::Table.new(sources, entries, nil)])
|
|
20
|
+
# chain.attributes('email', virtual: false, editable: true) # => { level: 'masked', ... } or nil
|
|
21
|
+
class FieldChain
|
|
22
|
+
# One entry table (`fields`: name => Admin::EffectiveFields entry): the
|
|
23
|
+
# resource's own (decided_by nil) or a base's (decided_by: the base
|
|
24
|
+
# Resource's name).
|
|
25
|
+
Table = Struct.new(:sources, :fields, :decided_by) do
|
|
26
|
+
def readable? = sources.flags[:read_access].granted?
|
|
27
|
+
# Whether a granting set grants read, muted later or not.
|
|
28
|
+
def granted_readable? = sources.flags[:read_access].granted_by.any?
|
|
29
|
+
def rank_of(name) = PermissionForge::FieldLevel.rank(PermissionForge::FieldLevel.effective_level(fields[name]))
|
|
30
|
+
def provenance(name) = FieldProvenance.new(fields[name], decided_by: decided_by)
|
|
31
|
+
# The field's flags as the granting sets alone left them.
|
|
32
|
+
def granted(name) = fields[name][:before] || fields[name]
|
|
33
|
+
# Whether a muting set brought the field to its level.
|
|
34
|
+
def muted?(name) = provenance(name).mutes.any?
|
|
35
|
+
|
|
36
|
+
# A field hidden because this entry does not grant read (ResourceChain::HIDDEN_ENTRY).
|
|
37
|
+
def hidden_attributes = FieldProvenance.hidden_by(sources.flags[:read_access].muted_on, decided_by: decided_by)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @param tables [Array<Table>] the resource's first, then its bases' nearest first
|
|
41
|
+
def initialize(tables)
|
|
42
|
+
@tables = tables
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Every field name an entry along the chain has.
|
|
46
|
+
#
|
|
47
|
+
# @return [Array<String>]
|
|
48
|
+
def names = @tables.flat_map { |table| table.fields.keys }
|
|
49
|
+
|
|
50
|
+
# The FieldOutcome attributes but its name, from the entry that decides;
|
|
51
|
+
# nil when no entry configures the field.
|
|
52
|
+
#
|
|
53
|
+
# @return [Hash, nil]
|
|
54
|
+
def attributes(name, virtual:, editable:)
|
|
55
|
+
configured = @tables.select { |table| table.fields.key?(name) }
|
|
56
|
+
return if configured.empty?
|
|
57
|
+
return chain_mutes(hiding.hidden_attributes.merge(virtual: virtual), configured, name) if hiding
|
|
58
|
+
|
|
59
|
+
chain_mutes(deciding(configured, name).provenance(name).attributes(virtual: virtual, editable: editable), configured, name)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Mutes that hide the field, or take its editing away, which the
|
|
63
|
+
# granting sets alone (composed along the chain) never showed, or never
|
|
64
|
+
# let be edited, are not credited. One entry: FieldProvenance did it.
|
|
65
|
+
CHAIN_MUTE_EFFECTS = {
|
|
66
|
+
'hide' => ->(granted) { granted[:read_access] == true },
|
|
67
|
+
'no_edit' => ->(granted) { granted[:edit_access] == true }
|
|
68
|
+
}.freeze
|
|
69
|
+
private_constant :CHAIN_MUTE_EFFECTS
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def chain_mutes(attributes, configured, name)
|
|
74
|
+
return attributes if @tables.size <= 1
|
|
75
|
+
|
|
76
|
+
granted = granted_composition(configured, name)
|
|
77
|
+
mutes = attributes[:mutes].select { |mute| CHAIN_MUTE_EFFECTS.fetch(mute.level, ->(_) { true }).call(granted) }
|
|
78
|
+
attributes.merge(mutes: mutes, blocked_by: mutes.map(&:permission_set), blocked_level: mutes.first&.level)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# The field as the granting sets alone compose it (ResourceChain.compose_field).
|
|
82
|
+
def granted_composition(configured, name)
|
|
83
|
+
return ResourceChain::HIDDEN_ENTRY unless @tables.all?(&:granted_readable?)
|
|
84
|
+
|
|
85
|
+
ResourceChain.strictest(configured.map { |table| table.granted(name) })
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Among several entries, the first that does not grant read.
|
|
89
|
+
def hiding
|
|
90
|
+
return @hiding if defined?(@hiding)
|
|
91
|
+
|
|
92
|
+
@hiding = (@tables.find { |table| !table.readable? } if @tables.size > 1)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# The strictest entry; at one level the first in chain order, but at
|
|
96
|
+
# Hidden or Read-only one no mute brought there first (the masked
|
|
97
|
+
# strategy is the first masking entry's, as the engine takes it).
|
|
98
|
+
def deciding(configured, name)
|
|
99
|
+
configured.each_with_index.min_by do |table, index|
|
|
100
|
+
rank = table.rank_of(name)
|
|
101
|
+
[rank, rank == MASKED_RANK || !table.muted?(name) ? 0 : 1, index]
|
|
102
|
+
end.first
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
MASKED_RANK = PermissionForge::FieldLevel.rank('masked')
|
|
106
|
+
private_constant :MASKED_RANK
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -49,15 +49,26 @@ module PermissionForge
|
|
|
49
49
|
# Locked when the field, its Resource or its Permission Set is (is_editable: false).
|
|
50
50
|
def locked? = !EditableLock.locked_record(field, resource).nil?
|
|
51
51
|
|
|
52
|
-
# The mask strategy the select starts on: the saved one
|
|
52
|
+
# The mask strategy the select starts on: the saved one — also one this
|
|
53
|
+
# app does not know (unknown_strategy?), which stays as stored unless
|
|
54
|
+
# the admin picks another and is masked fully meanwhile — else the
|
|
53
55
|
# recommended one for a sensitive column, else 'full'.
|
|
54
56
|
def strategy
|
|
55
|
-
field&.mask_strategy || row.match&.strategy || DEFAULT_STRATEGY
|
|
57
|
+
field&.mask_strategy.presence || row.match&.strategy || DEFAULT_STRATEGY
|
|
56
58
|
end
|
|
57
59
|
|
|
58
|
-
#
|
|
60
|
+
# The choices of the strategy select (MaskStrategyChoices): the saved
|
|
61
|
+
# one this app does not know is offered as stored, so saving the drawer
|
|
62
|
+
# with another change never rewrites it.
|
|
63
|
+
def strategy_choices = @strategy_choices ||= MaskStrategyChoices.new(field)
|
|
64
|
+
|
|
65
|
+
# The saved strategy is none this app knows: masked fully until fixed.
|
|
66
|
+
def unknown_strategy? = strategy_choices.unknown?
|
|
67
|
+
|
|
68
|
+
# Other names of the model that follow this one (`_id` → `_uuid`,
|
|
69
|
+
# `_cents` → base name), as the engine applies them (FieldVariants).
|
|
59
70
|
def variants
|
|
60
|
-
@variants ||= @configuration.
|
|
71
|
+
@variants ||= FieldVariants.new(resource.name, @configuration).call(key)
|
|
61
72
|
end
|
|
62
73
|
|
|
63
74
|
# The edit level needs edit access on the Resource, except in a muting set.
|
|
@@ -67,19 +78,21 @@ module PermissionForge
|
|
|
67
78
|
|
|
68
79
|
# The note under the level control while no setting is in force, as the
|
|
69
80
|
# suffix of a locale key under `resource_fields.show` — or nil.
|
|
81
|
+
# In a Muting Permission Set a field without a setting takes nothing
|
|
82
|
+
# away (no strategy applies), so its note says that instead.
|
|
70
83
|
def unconfigured_note
|
|
71
84
|
return nil if row.in_force?
|
|
72
85
|
|
|
73
|
-
|
|
74
|
-
if
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
implicit ? :not_configured_implicit : :not_configured
|
|
78
|
-
end
|
|
86
|
+
base = row.configured? ? :turned_off : :not_configured
|
|
87
|
+
return :"#{base}_muting" if muting?
|
|
88
|
+
|
|
89
|
+
row.implicit_grant ? :"#{base}_implicit" : base
|
|
79
90
|
end
|
|
80
91
|
|
|
81
|
-
# Setting the note links to.
|
|
92
|
+
# Setting the note links to; nil in a Muting Permission Set (no setting decides it).
|
|
82
93
|
def unconfigured_setting
|
|
94
|
+
return nil if muting?
|
|
95
|
+
|
|
83
96
|
row.implicit_grant ? 'extra_fields_default_access' : 'unconfigured_field_strategy'
|
|
84
97
|
end
|
|
85
98
|
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
# The one definition of the label the admin shows for a stored field,
|
|
6
|
+
# column or association name: the host application's own translation
|
|
7
|
+
# when it has one (`activerecord.attributes.<model>.<name>`, read
|
|
8
|
+
# through `human_attribute_name` with a sentinel default), else the label
|
|
9
|
+
# an admin wrote on the ResourceField row — a label the gem generated
|
|
10
|
+
# (PermissionForge::GeneratedText.label?: blank, the name, the name
|
|
11
|
+
# humanized, 'Untitled Field') is no label — else, on an English page,
|
|
12
|
+
# Rails' own English for the name (`human_attribute_name`: "Created at",
|
|
13
|
+
# what an English host saw before 0.15; #english), else the name
|
|
14
|
+
# verbatim (`verbatim?`: the view draws it in <code>; an admin looks for
|
|
15
|
+
# the stored name). Rails' English is never shown on a page in another
|
|
16
|
+
# language (release_readiness/inflection_guard_test.rb). A column with
|
|
17
|
+
# no row and one with a generated label read the same (field test round
|
|
18
|
+
# 7, B#26).
|
|
19
|
+
#
|
|
20
|
+
# @example
|
|
21
|
+
# FieldLabel.for(Customer, 'full_address').text # => "Adresa" (host translation); on sq "full_address", on en "Full address"
|
|
22
|
+
# FieldLabel.for('Customer', 'email', row: field).text # => the label an admin wrote, when the host has no translation
|
|
23
|
+
#
|
|
24
|
+
# @api private
|
|
25
|
+
class FieldLabel
|
|
26
|
+
# What `human_attribute_name` / `model_name.human` answer when the host
|
|
27
|
+
# has no translation (their `default:`): a value never shown.
|
|
28
|
+
MISSING = "\u0000permission_forge:missing\u0000"
|
|
29
|
+
|
|
30
|
+
# A label and whether it is the stored name itself.
|
|
31
|
+
Label = Data.define(:text, :verbatim) do
|
|
32
|
+
def verbatim? = verbatim
|
|
33
|
+
def to_s = text
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class << self
|
|
37
|
+
# @param model [Class, ModelCatalog::ModelInfo, String, nil] the model the name belongs to
|
|
38
|
+
# @param name [String, Symbol] the stored name
|
|
39
|
+
# @param row [#label, nil] the ResourceField (anything with a `label`) for the name
|
|
40
|
+
# @return [Label]
|
|
41
|
+
def for(model, name, row: nil)
|
|
42
|
+
name = name.to_s
|
|
43
|
+
translated = host_translation(model, name)
|
|
44
|
+
return Label.new(text: translated, verbatim: false) if translated
|
|
45
|
+
|
|
46
|
+
label = written_label(row, name)
|
|
47
|
+
return Label.new(text: label, verbatim: false) if label
|
|
48
|
+
|
|
49
|
+
fallback(klass_of(model), name)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The label an admin wrote on `row`, or nil for none — a label the
|
|
53
|
+
# gem generated (GeneratedText.label?) is none.
|
|
54
|
+
#
|
|
55
|
+
# @return [String, nil]
|
|
56
|
+
def written_label(row, name)
|
|
57
|
+
label = row.respond_to?(:label) ? row.label.to_s.strip : ''
|
|
58
|
+
label unless PermissionForge::GeneratedText.label?(label, name)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# A name with no translation and no label of the admin's: Rails' own
|
|
62
|
+
# English on an English page (#english), the name verbatim elsewhere
|
|
63
|
+
# — and wherever the model cannot be loaded (the stored name is what
|
|
64
|
+
# the admin looks for).
|
|
65
|
+
#
|
|
66
|
+
# @param klass [Class, nil] the model the name belongs to, when known
|
|
67
|
+
# @return [Label]
|
|
68
|
+
def fallback(klass, name)
|
|
69
|
+
text = english(name) { klass.human_attribute_name(name) if klass.respond_to?(:human_attribute_name) }
|
|
70
|
+
text ? Label.new(text: text, verbatim: false) : Label.new(text: name.to_s, verbatim: true)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Rails' English for a name, only while the page is English: what
|
|
74
|
+
# the block answers (a model's `human_attribute_name`: "Created at"),
|
|
75
|
+
# or without a block Rails' humanization of the name (an action key,
|
|
76
|
+
# which belongs to no attribute: "Export csv"). nil on any other
|
|
77
|
+
# page, and for a blank name.
|
|
78
|
+
#
|
|
79
|
+
# @return [String, nil]
|
|
80
|
+
def english(name)
|
|
81
|
+
return nil if name.blank? || !I18n.locale.to_s.start_with?('en')
|
|
82
|
+
|
|
83
|
+
block_given? ? yield : ActiveSupport::Inflector.humanize(name)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# The host's translation of `name` on `model`, or nil. An empty name
|
|
87
|
+
# (a doubled dot in a path) is no attribute: never asked.
|
|
88
|
+
#
|
|
89
|
+
# @return [String, nil]
|
|
90
|
+
def host_translation(model, name)
|
|
91
|
+
klass = klass_of(model)
|
|
92
|
+
return nil if name.blank? || !klass.respond_to?(:human_attribute_name)
|
|
93
|
+
|
|
94
|
+
value = klass.human_attribute_name(name, default: MISSING)
|
|
95
|
+
value == MISSING ? nil : value
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# The model class behind a Class, a ModelCatalog::ModelInfo or a
|
|
99
|
+
# stored Resource name (validated by Resource::NAME_FORMAT).
|
|
100
|
+
def klass_of(model)
|
|
101
|
+
return model.klass if model.respond_to?(:klass)
|
|
102
|
+
return PermissionForge::ModelLookup.model_for(model) if model.is_a?(String) || model.is_a?(Symbol)
|
|
103
|
+
|
|
104
|
+
model
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -2,73 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module PermissionForge
|
|
4
4
|
module Admin
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
|
|
9
|
-
module FieldLevel
|
|
10
|
-
GRANTING = {
|
|
11
|
-
'hidden' => { read_access: false, edit_access: false, mask_access: false },
|
|
12
|
-
'masked' => { read_access: true, edit_access: false, mask_access: true },
|
|
13
|
-
'read' => { read_access: true, edit_access: false, mask_access: false },
|
|
14
|
-
'edit' => { read_access: true, edit_access: true, mask_access: false }
|
|
15
|
-
}.freeze
|
|
16
|
-
|
|
17
|
-
MUTING = {
|
|
18
|
-
'leave' => { read_access: false, edit_access: false, mask_access: false },
|
|
19
|
-
'mask' => { read_access: false, edit_access: false, mask_access: true },
|
|
20
|
-
'hide' => { read_access: true, edit_access: true, mask_access: false }
|
|
21
|
-
}.freeze
|
|
22
|
-
|
|
23
|
-
# Ordered from most closed to most open (granting) — used to rank levels.
|
|
24
|
-
GRANTING_ORDER = %w[hidden masked read edit].freeze
|
|
25
|
-
|
|
26
|
-
module_function
|
|
27
|
-
|
|
28
|
-
def levels_for(muting:)
|
|
29
|
-
muting ? MUTING.keys : GRANTING.keys
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def flags_for(level, muting:)
|
|
33
|
-
(muting ? MUTING : GRANTING).fetch(level.to_s) { raise ArgumentError, "Unknown field level: #{level.inspect}" }
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def valid?(level, muting:)
|
|
37
|
-
(muting ? MUTING : GRANTING).key?(level.to_s)
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
# Derives the level from a field's flags. Flag combinations the UI never
|
|
41
|
-
# writes (e.g. edit + mask) collapse to the closest level.
|
|
42
|
-
def level_of(field, muting:)
|
|
43
|
-
read = field.read_access
|
|
44
|
-
edit = field.edit_access
|
|
45
|
-
mask = field.mask_access
|
|
46
|
-
return muting_level(read, edit, mask) if muting
|
|
47
|
-
|
|
48
|
-
return 'hidden' unless read
|
|
49
|
-
return 'masked' if mask
|
|
50
|
-
|
|
51
|
-
edit ? 'edit' : 'read'
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def muting_level(read, edit, mask)
|
|
55
|
-
return 'hide' if read || edit
|
|
56
|
-
|
|
57
|
-
mask ? 'mask' : 'leave'
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
# Level an engine field-permission entry resolves to
|
|
61
|
-
# ({ read_access:, edit_access:, mask_access: } from the permissions hash).
|
|
62
|
-
def effective_level(entry)
|
|
63
|
-
return 'hidden' if entry.nil? || !entry[:read_access]
|
|
64
|
-
return 'masked' if entry[:mask_access]
|
|
65
|
-
|
|
66
|
-
entry[:edit_access] ? 'edit' : 'read'
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def rank(level)
|
|
70
|
-
GRANTING_ORDER.index(level.to_s) || -1
|
|
71
|
-
end
|
|
72
|
-
end
|
|
5
|
+
# The level tables moved to PermissionForge::FieldLevel in 0.15.0 (the
|
|
6
|
+
# row builder in lib/ reads them too). Internal API: the old name is
|
|
7
|
+
# the same module, kept for the views and tests that spell it out.
|
|
8
|
+
FieldLevel = PermissionForge::FieldLevel
|
|
73
9
|
end
|
|
74
10
|
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
# The outcome of one effective field entry, and which Permission Sets it
|
|
6
|
+
# credits (EX: the explanation comes from the entry that decides, never a
|
|
7
|
+
# second walk).
|
|
8
|
+
# The entry is Admin::EffectiveFields': the engine's own flags
|
|
9
|
+
# (FieldPermissionMerger.merge_row, then the muting phase), plus
|
|
10
|
+
# `sources` ([set, row level, row strategy, Resource] in merge order),
|
|
11
|
+
# `mutes` ([set, level, Resource] in muting order) and `before` (the
|
|
12
|
+
# flags before muting). Each FieldSource names the Resource whose row
|
|
13
|
+
# it credits (B#38).
|
|
14
|
+
#
|
|
15
|
+
# - grants: the sets that make the field readable, one each at its most
|
|
16
|
+
# open level, most open first and, at one level, in merge order — the
|
|
17
|
+
# first one is the entry's level and, masked, the row whose strategy
|
|
18
|
+
# the entry took. A Hidden row grants nothing and has no say in the
|
|
19
|
+
# mask; an implicit (virtual-field) entry credits no set.
|
|
20
|
+
# - mutes: the muting sets that changed the entry (MUTE_EFFECTS), each at
|
|
21
|
+
# the strongest level it is credited with, the strongest first — the
|
|
22
|
+
# first one is the level the entry shows (hide → Hidden, mask → Masked,
|
|
23
|
+
# no_edit → Read-only). A mute that changed nothing (a field nobody
|
|
24
|
+
# shows, editing nobody gave) is not listed.
|
|
25
|
+
#
|
|
26
|
+
# @example
|
|
27
|
+
# FieldProvenance.new(entry, decided_by: nil).attributes(virtual: false, editable: true)
|
|
28
|
+
# # => { level: 'masked', capped: false, mask_strategy: 'email', grants: [FieldSource], mutes: [], ... }
|
|
29
|
+
class FieldProvenance
|
|
30
|
+
FieldSource = EffectiveAccess::FieldSource
|
|
31
|
+
|
|
32
|
+
# What a muting set does to a field, strongest first (FieldSource#level).
|
|
33
|
+
MUTING_ORDER = %w[hide mask no_edit].freeze
|
|
34
|
+
# Whether a muting level changed the entry: (flags before muting, entry
|
|
35
|
+
# after) => true. Hide took a readable field away; mask leaves it
|
|
36
|
+
# masked; no_edit took editing away and left it readable.
|
|
37
|
+
MUTE_EFFECTS = {
|
|
38
|
+
'hide' => ->(before, after) { before[:read_access] == true && !after[:read_access] },
|
|
39
|
+
'mask' => ->(before, after) { before[:mask_access] != true && PermissionForge::FieldLevel.effective_level(after) == 'masked' },
|
|
40
|
+
'no_edit' => ->(before, after) { before[:edit_access] == true && after[:read_access] == true && !after[:edit_access] }
|
|
41
|
+
}.freeze
|
|
42
|
+
|
|
43
|
+
# The attributes of a field hidden because an entry does not grant
|
|
44
|
+
# read (ResourceChain::HIDDEN_ENTRY): no set grants it; the muting sets
|
|
45
|
+
# that took that entry's read away hide it.
|
|
46
|
+
#
|
|
47
|
+
# @param muted_on [Hash{PermissionForge::PermissionSet => String}] each
|
|
48
|
+
# muting set => the Resource whose row takes the read away
|
|
49
|
+
# (EffectiveAccess::Outcome#muted_on)
|
|
50
|
+
# @param decided_by [String, nil] the entry's Resource, for a base's
|
|
51
|
+
# @return [Hash] EffectiveAccess::FieldOutcome attributes but name and virtual
|
|
52
|
+
def self.hidden_by(muted_on, decided_by:)
|
|
53
|
+
mutes = muted_on.map { |set, resource_name| FieldSource.new(permission_set: set, level: 'hide', resource_name: resource_name) }
|
|
54
|
+
{ level: 'hidden', capped: false, unconfigured: false, implicit: false, unreadable: true, decided_by: decided_by,
|
|
55
|
+
mask_strategy: ResourceChain::HIDDEN_ENTRY[:mask_strategy], granted_by: [], grants: [], blocked_by: muted_on.keys,
|
|
56
|
+
mutes: mutes, blocked_level: mutes.first&.level }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @param entry [Hash] an Admin::EffectiveFields entry
|
|
60
|
+
# @param decided_by [String, nil] the base Resource whose entry it is
|
|
61
|
+
# (single-table inheritance), nil for the resource's own
|
|
62
|
+
def initialize(entry, decided_by: nil)
|
|
63
|
+
@entry = entry
|
|
64
|
+
@decided_by = decided_by
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# The entry's outcome and the sets it credits, as EffectiveAccess::FieldOutcome takes them.
|
|
68
|
+
#
|
|
69
|
+
# @param virtual [Boolean] the model declares the field (not a column)
|
|
70
|
+
# @param editable [Boolean] the resource grants Edit: an editable field
|
|
71
|
+
# is capped to Read-only without it, and a mute that takes its
|
|
72
|
+
# editing away changes nothing then (not credited; review of round 8, D3)
|
|
73
|
+
# @return [Hash] every FieldOutcome attribute but name and unconfigured
|
|
74
|
+
def attributes(virtual:, editable:)
|
|
75
|
+
granting = grants
|
|
76
|
+
muting = editable ? mutes : mutes.reject { |source| source.level == 'no_edit' }
|
|
77
|
+
{ virtual: virtual || implicit?, implicit: implicit?, mask_strategy: @entry[:mask_strategy],
|
|
78
|
+
variant_of: @entry[:variant_of], decided_by: @decided_by, unreadable: false, **level(editable),
|
|
79
|
+
granted_by: granting.map(&:permission_set), grants: granting, blocked_by: muting.map(&:permission_set),
|
|
80
|
+
mutes: muting, blocked_level: muting.first&.level }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# An implicit (virtual-field) entry: no set configured the field.
|
|
84
|
+
def implicit? = @entry[:extra].present?
|
|
85
|
+
|
|
86
|
+
# @return [Array<EffectiveAccess::FieldSource>]
|
|
87
|
+
def grants
|
|
88
|
+
return [] if implicit?
|
|
89
|
+
|
|
90
|
+
rows = @entry[:sources].each_with_index.select { |(_, level), _| FieldLevel.rank(level) >= 1 }
|
|
91
|
+
rows.sort_by { |(_, level), index| [-FieldLevel.rank(level), index] }.map(&:first).uniq(&:first)
|
|
92
|
+
.map { |set, level, strategy, name| FieldSource.new(permission_set: set, level: level, mask_strategy: strategy, resource_name: name) }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# @return [Array<EffectiveAccess::FieldSource>]
|
|
96
|
+
def mutes
|
|
97
|
+
sources = credited_mutes.group_by(&:first).map { |set, rows| strongest(set, rows) }
|
|
98
|
+
sources.each_with_index.sort_by { |source, index| [MUTING_ORDER.index(source.level), index] }.map(&:first)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
# [set, level, Resource] for each muting row or resource that changed the entry.
|
|
104
|
+
def credited_mutes
|
|
105
|
+
(@entry[:mutes] || []).select { |_, level| changed?(level) }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# The set at its strongest level, through its first row at that level.
|
|
109
|
+
def strongest(permission_set, rows)
|
|
110
|
+
_, level, resource_name = MUTING_ORDER.lazy.filter_map { |name| rows.find { |_, row_level| row_level == name } }.first
|
|
111
|
+
FieldSource.new(permission_set: permission_set, level: level, resource_name: resource_name)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# The entry's level (FieldLevel.effective_level); Edit is capped to
|
|
115
|
+
# Read-only when the resource does not grant Edit. Then a mute that
|
|
116
|
+
# took the field's editing away changed nothing (D3): the field is
|
|
117
|
+
# capped as the granting sets left its editing.
|
|
118
|
+
def level(editable)
|
|
119
|
+
level = FieldLevel.effective_level(editable ? @entry : @entry.merge(edit_access: granted_edit?))
|
|
120
|
+
capped = level == 'edit' && !editable
|
|
121
|
+
{ level: capped ? 'read' : level, capped: capped }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Whether the field is readable and the granting sets let it be edited.
|
|
125
|
+
def granted_edit? = @entry[:read_access] == true && (@entry[:before] || @entry)[:edit_access] == true
|
|
126
|
+
|
|
127
|
+
def changed?(level)
|
|
128
|
+
MUTE_EFFECTS.fetch(level).call(@entry[:before] || {}, @entry)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -4,7 +4,10 @@ module PermissionForge
|
|
|
4
4
|
module Admin
|
|
5
5
|
# One row per field on a Resource page: configured ResourceFields, model
|
|
6
6
|
# columns nobody configured yet, and virtual fields — with the flags the
|
|
7
|
-
# UI needs (sensitive, virtual, column missing, level).
|
|
7
|
+
# UI needs (sensitive, virtual, column missing, level). A row's label is
|
|
8
|
+
# Admin::FieldLabel's, the same for a configured row and a column with
|
|
9
|
+
# none (field test round 7, B#26): the host's translation, else the
|
|
10
|
+
# admin's label, else Rails' English on an English page, else the name.
|
|
8
11
|
class FieldRows
|
|
9
12
|
# `declared` — a virtual field the model lists in extra_fields_method
|
|
10
13
|
# (a virtual row that is not declared was discovered). `implicit_grant` —
|
|
@@ -15,7 +18,12 @@ module PermissionForge
|
|
|
15
18
|
keyword_init: true) do
|
|
16
19
|
def configured? = !field.nil?
|
|
17
20
|
def sensitive? = !match.nil?
|
|
18
|
-
|
|
21
|
+
# The label is the name itself (FieldLabel: no translation, no label
|
|
22
|
+
# of the admin's, a page that is not English): the grid shows the
|
|
23
|
+
# name once, in <code>.
|
|
24
|
+
def verbatim? = label == name
|
|
25
|
+
# Locked when the row, its Resource or its Permission Set is (EditableLock).
|
|
26
|
+
def locked? = !field.nil? && EditableLock.locked?(field)
|
|
19
27
|
def active? = field.nil? || field.is_active
|
|
20
28
|
|
|
21
29
|
# A setting the engine applies: it exists and is turned on. A row that
|
|
@@ -25,7 +33,10 @@ module PermissionForge
|
|
|
25
33
|
end
|
|
26
34
|
|
|
27
35
|
GRANTING_FILTERS = %w[all hidden masked read edit sensitive unconfigured].freeze
|
|
28
|
-
|
|
36
|
+
# No "Not configured" here: in a Muting Permission Set a column with no
|
|
37
|
+
# setting mutes nothing, which is what Unchanged means — one state, one
|
|
38
|
+
# name (its row starts on Unchanged, and saving it creates no row).
|
|
39
|
+
MUTING_FILTERS = %w[all leave mask hide sensitive].freeze
|
|
29
40
|
|
|
30
41
|
attr_reader :resource, :model
|
|
31
42
|
|
|
@@ -78,6 +89,13 @@ module PermissionForge
|
|
|
78
89
|
new_columns.any? || missing_fields.any?
|
|
79
90
|
end
|
|
80
91
|
|
|
92
|
+
# Whether the sync panel is a warning. A column with no setting in a
|
|
93
|
+
# Muting Permission Set takes nothing away — the normal state, not a
|
|
94
|
+
# gap — so there only a configured field whose column is gone warns.
|
|
95
|
+
def sync_warning?
|
|
96
|
+
missing_fields.any? || (!@muting && new_columns.any?)
|
|
97
|
+
end
|
|
98
|
+
|
|
81
99
|
private
|
|
82
100
|
|
|
83
101
|
# Every row counts, switched off or not: a field with a row cannot get a
|
|
@@ -93,16 +111,17 @@ module PermissionForge
|
|
|
93
111
|
def configured_row(field)
|
|
94
112
|
name = field.name
|
|
95
113
|
virtual = virtual?(name)
|
|
96
|
-
Row.new(name: name, label:
|
|
97
|
-
virtual: virtual, missing: @model.present? && !virtual &&
|
|
114
|
+
Row.new(name: name, label: FieldLabel.for(@model, name, row: field).text, field: field, match: @sensitive.match(name),
|
|
115
|
+
virtual: virtual, missing: @model.present? && !virtual && !@model.column?(name),
|
|
98
116
|
level: FieldLevel.level_of(field, muting: @muting), declared: declared?(name),
|
|
99
117
|
implicit_grant: !field.is_active && implicit_grant?(name))
|
|
100
118
|
end
|
|
101
119
|
|
|
102
120
|
def unconfigured_rows
|
|
121
|
+
level = FieldLevel.unconfigured_level(muting: @muting)
|
|
103
122
|
new_columns.map do |name|
|
|
104
|
-
Row.new(name: name, label:
|
|
105
|
-
missing: false, level:
|
|
123
|
+
Row.new(name: name, label: FieldLabel.for(@model, name).text, field: nil, match: @sensitive.match(name), virtual: virtual?(name),
|
|
124
|
+
missing: false, level: level, declared: declared?(name), implicit_grant: implicit_grant?(name))
|
|
106
125
|
end
|
|
107
126
|
end
|
|
108
127
|
|
|
@@ -120,10 +139,6 @@ module PermissionForge
|
|
|
120
139
|
@model.present? && @model.declared_virtual_field_names.include?(name)
|
|
121
140
|
end
|
|
122
141
|
|
|
123
|
-
def human(name)
|
|
124
|
-
@model ? @model.klass.human_attribute_name(name) : name.to_s.humanize
|
|
125
|
-
end
|
|
126
|
-
|
|
127
142
|
def match_filter?(row, filter)
|
|
128
143
|
case filter
|
|
129
144
|
when 'all' then true
|
|
@@ -9,12 +9,26 @@ module PermissionForge
|
|
|
9
9
|
# an edit by nobody, by an HTTP Basic login or by an API client keeps the
|
|
10
10
|
# previous editor — so wherever the admin shows "last edited by", an
|
|
11
11
|
# event wins over the columns (see AuditTrail::Entry and the footer).
|
|
12
|
+
# The same holds for "created by" (.creations).
|
|
12
13
|
#
|
|
13
14
|
# Two queries per record type and slice: the newest event id per record
|
|
14
15
|
# (the ids grow with every insert), then those events.
|
|
15
16
|
class LastChanges
|
|
16
17
|
SLICE = 500
|
|
17
18
|
|
|
19
|
+
# The creation event of each record — its model's `audit_event_name(:create)`:
|
|
20
|
+
# permission_set.created, assignment.granted, group_membership.added, ... —
|
|
21
|
+
# read with #[]. Who created a record is that event's actor wherever the
|
|
22
|
+
# admin shows it (AuditHelper#pf_audit_creator): an HTTP Basic login or an
|
|
23
|
+
# API client stamps no created_by, and the audit log still names them.
|
|
24
|
+
#
|
|
25
|
+
# @param records [Array<ActiveRecord::Base>] permission records
|
|
26
|
+
# @return [LastChanges]
|
|
27
|
+
def self.creations(records)
|
|
28
|
+
records = Array(records).compact
|
|
29
|
+
new(records, events: records.map { |record| record.class.audit_event_name(:create) }.uniq)
|
|
30
|
+
end
|
|
31
|
+
|
|
18
32
|
# @param records [Array<ActiveRecord::Base>] permission records
|
|
19
33
|
# @param events [Array<String>, nil] only these change events (e.g.
|
|
20
34
|
# %w[assignment.granted] — who granted it); nil: any change event
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
# A list as a sentence ("Bob, Carol and Dan") with the connectors of the
|
|
6
|
+
# current locale, read from the gem's own keys
|
|
7
|
+
# (permission_forge.support.array.*): Array#to_sentence reads Rails'
|
|
8
|
+
# support.array keys, which an app without rails-i18n has in English only,
|
|
9
|
+
# so a Macedonian page would read "Bob and Carol". The admin joins every
|
|
10
|
+
# list through here (helper: pf_to_sentence); a test fails on a bare
|
|
11
|
+
# to_sentence in app/ or lib/.
|
|
12
|
+
#
|
|
13
|
+
# @api private
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# ListSentence.call(%w[Bob Carol Dan]) # => "Bob, Carol and Dan" (en)
|
|
17
|
+
module ListSentence
|
|
18
|
+
# @param items [Array<#to_s>]
|
|
19
|
+
# @param overrides [Hash] connectors that replace the locale's (e.g. "or")
|
|
20
|
+
# @return [String]
|
|
21
|
+
def self.call(items, **overrides)
|
|
22
|
+
Array(items).to_sentence(**connectors, **overrides)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.connectors
|
|
26
|
+
{ words_connector: I18n.t('permission_forge.support.array.words_connector'),
|
|
27
|
+
two_words_connector: I18n.t('permission_forge.support.array.two_words_connector'),
|
|
28
|
+
last_word_connector: I18n.t('permission_forge.support.array.last_word_connector') }
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|