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
|
@@ -22,9 +22,11 @@ module PermissionForge
|
|
|
22
22
|
new(permission_set, attrs, unlock: unlock).call
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
# Without a name the copy is named "<name> (copy)", numbered when taken
|
|
26
|
+
# (names are unique per tenant: PermissionSet.copy_name).
|
|
25
27
|
def initialize(permission_set, attrs, unlock: false)
|
|
26
28
|
@permission_set = permission_set
|
|
27
|
-
@attrs = attrs
|
|
29
|
+
@attrs = attrs.key?(:name) || attrs.key?('name') ? attrs : attrs.merge(name: PermissionForge::PermissionSet.copy_name(permission_set.name))
|
|
28
30
|
@unlock = unlock
|
|
29
31
|
end
|
|
30
32
|
|
|
@@ -42,29 +44,51 @@ module PermissionForge
|
|
|
42
44
|
|
|
43
45
|
private
|
|
44
46
|
|
|
47
|
+
# The rows in the order the hash builder merges them
|
|
48
|
+
# (PermissionHashBuilder.sort_rows, the one definition of row order).
|
|
45
49
|
def clone_resources(target_set)
|
|
46
|
-
@
|
|
47
|
-
|
|
48
|
-
|
|
50
|
+
@skipped = []
|
|
51
|
+
@permission_set.resources.includes(:resource_fields, :resource_actions, :permission_set_rules).load
|
|
52
|
+
PermissionForge::Services::PermissionHashBuilder.sort_rows(@permission_set)
|
|
53
|
+
@permission_set.resources.each { |resource| clone_resource(target_set, resource) }
|
|
54
|
+
log_skipped
|
|
49
55
|
end
|
|
50
56
|
|
|
57
|
+
# A field or action row with a blank name (written before the names
|
|
58
|
+
# were checked) cannot be created again: the copy leaves it out —
|
|
59
|
+
# such a row names no column and no action — and the log says so,
|
|
60
|
+
# once per copy, instead of refusing the whole copy.
|
|
51
61
|
def clone_resource(target_set, resource)
|
|
52
62
|
cloned_resource = copy(resource, permission_set: target_set)
|
|
53
63
|
|
|
54
|
-
resource.resource_fields.each { |field| copy(field, resource: cloned_resource) }
|
|
64
|
+
copyable(resource.resource_fields).each { |field| copy(field, resource: cloned_resource) }
|
|
55
65
|
clone_actions(resource, cloned_resource)
|
|
56
66
|
resource.permission_set_rules.each { |rule| copy(rule, permission_set: target_set, resource: cloned_resource) }
|
|
57
67
|
end
|
|
58
68
|
|
|
59
69
|
# One copy per action name. Rows that share a name (possible only on a
|
|
60
|
-
# table without the unique index) are OR-ed when permissions are built
|
|
61
|
-
#
|
|
70
|
+
# table without the unique index) are OR-ed when permissions are built
|
|
71
|
+
# (PermissionHashBuilder#merge_resource_actions: the active rows'
|
|
72
|
+
# is_permitted, ||=), so the copy is the single row they amount to.
|
|
62
73
|
def clone_actions(resource, cloned_resource)
|
|
63
|
-
resource.resource_actions.group_by(&:name).each_value do |rows|
|
|
74
|
+
copyable(resource.resource_actions).group_by(&:name).each_value do |rows|
|
|
64
75
|
copy(rows.first, resource: cloned_resource, **combined_action_flags(rows))
|
|
65
76
|
end
|
|
66
77
|
end
|
|
67
78
|
|
|
79
|
+
def copyable(rows)
|
|
80
|
+
kept, skipped = rows.partition { |row| !refused_for_blank_name?(row) }
|
|
81
|
+
@skipped.concat(skipped)
|
|
82
|
+
kept
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def log_skipped
|
|
86
|
+
return if @skipped.empty?
|
|
87
|
+
|
|
88
|
+
kinds = @skipped.map { |row| row.class.name.demodulize }.tally.map { |kind, count| "#{count} #{kind}" }.join(', ')
|
|
89
|
+
Rails.logger&.warn("[PermissionForge] copying #{@permission_set.name}: skipped #{@skipped.size} row(s) with a blank name (#{kinds})")
|
|
90
|
+
end
|
|
91
|
+
|
|
68
92
|
def combined_action_flags(rows)
|
|
69
93
|
return {} if rows.one?
|
|
70
94
|
|
|
@@ -6,6 +6,12 @@ module PermissionForge
|
|
|
6
6
|
# Returns true if the set is active for the given context (all conditions pass).
|
|
7
7
|
# A nil/blank condition always passes (unconditional).
|
|
8
8
|
#
|
|
9
|
+
# The conditions are read through PermissionForge::ActivationConditions
|
|
10
|
+
# (the one parser of the IP list and the hour window). A condition nobody
|
|
11
|
+
# can judge — an IP entry that is not an address, a list of separators,
|
|
12
|
+
# one hour of the two, the same hour twice, an hour outside 0..23 — never
|
|
13
|
+
# passes (.broken?); ConditionalSets applies a muting set with one.
|
|
14
|
+
#
|
|
9
15
|
# @example
|
|
10
16
|
# PermissionSetConditionEvaluator.call(
|
|
11
17
|
# permission_set: ps,
|
|
@@ -22,22 +28,35 @@ module PermissionForge
|
|
|
22
28
|
new(permission_set, context).call
|
|
23
29
|
end
|
|
24
30
|
|
|
25
|
-
# Whether a condition of the set cannot be judged in `context`:
|
|
31
|
+
# Whether a condition of the set cannot be judged in `context`: the
|
|
32
|
+
# set holds a broken condition (.broken?, whatever the context), it
|
|
26
33
|
# restricts the IP and the context has none, or it requires a session
|
|
27
34
|
# flag and the context says nothing about flags (no :session_flags at
|
|
28
35
|
# all — an empty list is an answer). The hours are always judged (the
|
|
29
|
-
# time defaults to now).
|
|
30
|
-
#
|
|
36
|
+
# time defaults to now). A conditional muting set applies then under
|
|
37
|
+
# config.conditional_sets_strict_mode — and with a broken condition in
|
|
38
|
+
# every mode (ConditionalSets::Member#applies?).
|
|
31
39
|
#
|
|
32
40
|
# @return [Boolean]
|
|
33
41
|
def self.unjudgeable?(permission_set:, context:)
|
|
34
42
|
context ||= {}
|
|
35
|
-
(permission_set
|
|
43
|
+
broken?(permission_set: permission_set) ||
|
|
44
|
+
(permission_set.allowed_ips.present? && context[:ip].blank?) ||
|
|
36
45
|
(permission_set.requires_session_flag.present? && context[:session_flags].nil?)
|
|
37
46
|
end
|
|
38
47
|
|
|
48
|
+
# Whether the set holds a condition nobody can judge
|
|
49
|
+
# (ActivationConditions#conditions_broken?): such a set never passes.
|
|
50
|
+
#
|
|
51
|
+
# @param permission_set [PermissionSet, #allowed_ips]
|
|
52
|
+
# @return [Boolean]
|
|
53
|
+
def self.broken?(permission_set:)
|
|
54
|
+
PermissionForge::ActivationConditions.of(permission_set).conditions_broken?
|
|
55
|
+
end
|
|
56
|
+
|
|
39
57
|
def initialize(permission_set, context)
|
|
40
58
|
@permission_set = permission_set
|
|
59
|
+
@conditions = PermissionForge::ActivationConditions.of(permission_set)
|
|
41
60
|
@context = context || {}
|
|
42
61
|
end
|
|
43
62
|
|
|
@@ -57,45 +76,28 @@ module PermissionForge
|
|
|
57
76
|
end
|
|
58
77
|
|
|
59
78
|
# IP restriction: set only active when request IP matches allowed CIDRs.
|
|
60
|
-
# The IP may be a String or an IPAddr
|
|
61
|
-
#
|
|
79
|
+
# The IP may be a String or an IPAddr (ActivationConditions.request_address:
|
|
80
|
+
# an IPv4-mapped IPv6 address is read as its IPv4 address); anything
|
|
81
|
+
# that is not an address fails the condition, as does a list that
|
|
82
|
+
# cannot be judged.
|
|
62
83
|
def ip_passes?
|
|
63
|
-
|
|
64
|
-
return
|
|
84
|
+
return true if @permission_set.allowed_ips.blank?
|
|
85
|
+
return false if @conditions.ip_list_broken?
|
|
65
86
|
|
|
66
|
-
|
|
67
|
-
return false
|
|
87
|
+
address = PermissionForge::ActivationConditions.request_address(@context[:ip])
|
|
88
|
+
return false unless address
|
|
68
89
|
|
|
69
|
-
|
|
70
|
-
parse_ip_ranges(allowed).any? { |range| range.include?(address) }
|
|
71
|
-
rescue IPAddr::Error
|
|
72
|
-
false
|
|
90
|
+
@conditions.allowed_ip_ranges.any? { |range| range.include?(address) }
|
|
73
91
|
end
|
|
74
92
|
|
|
75
|
-
# Login hours: set only active during allowed hour window
|
|
93
|
+
# Login hours: set only active during the allowed hour window; a
|
|
94
|
+
# broken window (one hour, the same hour twice, out of range) includes
|
|
95
|
+
# no hour.
|
|
76
96
|
def hours_pass?
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return true if start_hour.nil? || end_hour.nil?
|
|
80
|
-
|
|
81
|
-
hour_in_range?((@context[:time] || Time.current).hour, start_hour, end_hour)
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def hour_in_range?(hour, start_hour, end_hour)
|
|
85
|
-
if start_hour <= end_hour
|
|
86
|
-
hour >= start_hour && hour < end_hour
|
|
87
|
-
else
|
|
88
|
-
# Wraps midnight (e.g., 22-06 means 22:00 to 05:59)
|
|
89
|
-
hour >= start_hour || hour < end_hour
|
|
90
|
-
end
|
|
91
|
-
end
|
|
97
|
+
window = @conditions.hours_window
|
|
98
|
+
return true unless window.set?
|
|
92
99
|
|
|
93
|
-
|
|
94
|
-
allowed_ips.split(',').filter_map do |cidr|
|
|
95
|
-
IPAddr.new(cidr.strip)
|
|
96
|
-
rescue IPAddr::Error
|
|
97
|
-
nil
|
|
98
|
-
end
|
|
100
|
+
window.include?((@context[:time] || Time.current).hour)
|
|
99
101
|
end
|
|
100
102
|
end
|
|
101
103
|
end
|
|
@@ -37,7 +37,8 @@ module PermissionForge
|
|
|
37
37
|
|
|
38
38
|
def clone_permission_sets(cloned_group)
|
|
39
39
|
@group.permission_sets.each do |ps|
|
|
40
|
-
|
|
40
|
+
# Names are unique per tenant: each copy is "<name> (copy)", numbered when taken.
|
|
41
|
+
attrs = { name: PermissionForge::PermissionSet.copy_name(ps.name), permission_set_group: cloned_group }
|
|
41
42
|
attrs[:is_editable] = true if @unlock
|
|
42
43
|
PermissionSetCloner.call(permission_set: ps, attrs: attrs, unlock: @unlock)
|
|
43
44
|
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Services
|
|
5
|
+
class PermitsAttributes
|
|
6
|
+
# What `PermitsAttributes.call(..., report: true)` answers: the filtered
|
|
7
|
+
# params and what was dropped from them.
|
|
8
|
+
#
|
|
9
|
+
# @!attribute [r] params
|
|
10
|
+
# @return [Hash] the filtered params, as `call` without `report:` returns them
|
|
11
|
+
# @!attribute [r] dropped
|
|
12
|
+
# @return [Array<String>] the paths of the dropped keys, in order, without
|
|
13
|
+
# repeats: `status`, `supplier_attributes.secret`,
|
|
14
|
+
# `lines_attributes.*.price`, `lines_attributes.*` (an element that is
|
|
15
|
+
# no record, or one whose id the user may not reach); never values
|
|
16
|
+
#
|
|
17
|
+
# @api public
|
|
18
|
+
Result = Data.define(:params, :dropped) do
|
|
19
|
+
# @return [Boolean] whether any key was dropped
|
|
20
|
+
def dropped? = dropped.any?
|
|
21
|
+
|
|
22
|
+
# The top-level keys behind the dropped paths (`lines_attributes`
|
|
23
|
+
# for `lines_attributes.*.price`), for a message such as
|
|
24
|
+
# "Not saved: status, notes".
|
|
25
|
+
#
|
|
26
|
+
# @return [Array<String>]
|
|
27
|
+
def dropped_fields = dropped.map { |path| path.split('.', 2).first }.uniq
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -31,8 +31,16 @@ module PermissionForge
|
|
|
31
31
|
# `lines_attributes.*` for an element that is no record — never their
|
|
32
32
|
# values. Without a user every key is dropped (reason nil_user).
|
|
33
33
|
#
|
|
34
|
+
# With `report: true` the call answers a {Result} instead: the same
|
|
35
|
+
# filtered hash plus the paths of the dropped keys, so a controller can
|
|
36
|
+
# tell the user what was not saved (the default return is unchanged).
|
|
37
|
+
#
|
|
34
38
|
# @example
|
|
35
39
|
# filtered = PermitsAttributes.call(resource: customer, permitted_params: params)
|
|
40
|
+
# @example Telling the user what was not saved
|
|
41
|
+
# result = PermitsAttributes.call(resource: customer, permitted_params: params, report: true)
|
|
42
|
+
# customer.update(result.params)
|
|
43
|
+
# flash[:alert] = "Not saved: #{result.dropped_fields.join(', ')}" if result.dropped?
|
|
36
44
|
#
|
|
37
45
|
# @api public
|
|
38
46
|
class PermitsAttributes
|
|
@@ -46,49 +54,61 @@ module PermissionForge
|
|
|
46
54
|
DESTROY_KEY = '_destroy'.freeze
|
|
47
55
|
|
|
48
56
|
# Filters incoming params by the user's field-level edit access (does not mutate them).
|
|
49
|
-
# @param resource [
|
|
57
|
+
# @param resource [ActiveRecord::Base, Class] the record or model class whose Resource rows decide
|
|
50
58
|
# @param permitted_params [Hash]
|
|
51
|
-
# @
|
|
52
|
-
|
|
53
|
-
|
|
59
|
+
# @param report [Boolean] answer a {Result} (filtered params and dropped paths)
|
|
60
|
+
# @return [Hash, Result] the filtered hash; a {Result} with `report: true`
|
|
61
|
+
def self.call(resource:, permitted_params:, report: false)
|
|
62
|
+
new(resource: resource, permitted_params: permitted_params, report: report).call
|
|
54
63
|
end
|
|
55
64
|
|
|
56
|
-
def initialize(resource:, permitted_params:)
|
|
65
|
+
def initialize(resource:, permitted_params:, report: false)
|
|
57
66
|
@user = PermissionForge.current_user
|
|
58
67
|
@resource = resource
|
|
59
68
|
@permitted_params = permitted_params
|
|
69
|
+
@report = report
|
|
60
70
|
end
|
|
61
71
|
|
|
62
|
-
# @return [Hash] a new hash holding only the fields the user may
|
|
72
|
+
# @return [Hash, Result] a new hash holding only the fields the user may
|
|
73
|
+
# edit; with `report: true`, a {Result} holding it and the dropped paths
|
|
63
74
|
def call
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
75
|
+
filtered = filter
|
|
76
|
+
@report ? Result.new(params: filtered, dropped: (@dropped || []).uniq) : filtered
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def filter
|
|
82
|
+
return drop_everything if resource_class.nil? || @user.blank?
|
|
69
83
|
return {} if @permitted_params.blank?
|
|
70
84
|
|
|
71
85
|
report_dropped('field_access') { filter_for(resource_class, @permitted_params) }
|
|
72
86
|
end
|
|
73
87
|
|
|
74
|
-
|
|
88
|
+
# Every key is dropped: nobody is signed in (published as nil_user), or
|
|
89
|
+
# there is no model to filter for (reported only: params.filtered
|
|
90
|
+
# names the model).
|
|
91
|
+
def drop_everything
|
|
92
|
+
if @permitted_params.present? && (@report || PermissionForge::Audit.instrument?(EVENT))
|
|
93
|
+
report_dropped('nil_user') { @permitted_params.each_pair { |key, _| dropped(key) } }
|
|
94
|
+
end
|
|
95
|
+
{}
|
|
96
|
+
end
|
|
75
97
|
|
|
76
98
|
# Runs the filter, collecting the dropped paths in @dropped only when
|
|
77
|
-
# params.filtered is stored or heard,
|
|
99
|
+
# the caller asked for a report or params.filtered is stored or heard,
|
|
100
|
+
# and publishes them to the latter.
|
|
78
101
|
def report_dropped(reason)
|
|
79
|
-
|
|
102
|
+
instrumented = !resource_class.nil? && PermissionForge::Audit.instrument?(EVENT)
|
|
103
|
+
return yield unless instrumented || @report
|
|
80
104
|
|
|
81
105
|
@dropped = []
|
|
82
106
|
@path = []
|
|
83
107
|
result = yield
|
|
84
|
-
publish_dropped(reason)
|
|
108
|
+
publish_dropped(reason) if instrumented && !@dropped.empty?
|
|
85
109
|
result
|
|
86
110
|
end
|
|
87
111
|
|
|
88
|
-
def nil_user_report?
|
|
89
|
-
!resource_class.nil? && @permitted_params.present? && PermissionForge::Audit.instrument?(EVENT)
|
|
90
|
-
end
|
|
91
|
-
|
|
92
112
|
def publish_dropped(reason)
|
|
93
113
|
dropped = @dropped.uniq
|
|
94
114
|
record = @resource.is_a?(Class) ? nil : @resource
|
|
@@ -130,7 +150,7 @@ module PermissionForge
|
|
|
130
150
|
fields = editable_fields_for(klass)
|
|
131
151
|
result = {}
|
|
132
152
|
|
|
133
|
-
each_pair
|
|
153
|
+
params.each_pair do |key, value|
|
|
134
154
|
if (association = nested_association(klass, key))
|
|
135
155
|
filtered = within(key) { filter_nested(klass, association, value) }
|
|
136
156
|
filtered.nil? ? dropped(key) : result[key] = filtered
|
|
@@ -230,7 +250,7 @@ module PermissionForge
|
|
|
230
250
|
|
|
231
251
|
def filter_indexed(target, records)
|
|
232
252
|
result = {}
|
|
233
|
-
each_pair
|
|
253
|
+
records.each_pair do |index, record|
|
|
234
254
|
filtered = within(EACH) { filter_record(target, record) }
|
|
235
255
|
filtered.nil? ? dropped(EACH) : result[index] = filtered
|
|
236
256
|
end
|
|
@@ -251,15 +271,16 @@ module PermissionForge
|
|
|
251
271
|
@user.respond_to?(:permissions_hash) && PermissionForge::ResourceChain.entries(@user, resource_names(klass)).any?
|
|
252
272
|
end
|
|
253
273
|
|
|
274
|
+
# A key is editable where the field is readable AND editable: the hash
|
|
275
|
+
# the builder writes says so already (FieldPermissionMerger counts edit
|
|
276
|
+
# only with read), but a hash another version cached may still say
|
|
277
|
+
# `edit_access: true, read_access: false` — a field nobody may read is
|
|
278
|
+
# not one they may write.
|
|
254
279
|
def editable_fields_for(klass)
|
|
255
280
|
return {} unless @user.respond_to?(:permissions_hash)
|
|
256
281
|
|
|
257
|
-
PermissionForge::ResourceChain.fields(@user, resource_names(klass))
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
# Iterates key/value pairs of an ActionController::Parameters or a Hash.
|
|
261
|
-
def each_pair(params, &)
|
|
262
|
-
params.each_pair(&)
|
|
282
|
+
PermissionForge::ResourceChain.fields(@user, resource_names(klass))
|
|
283
|
+
.select { |_, access| access[:read_access] && access[:edit_access] }
|
|
263
284
|
end
|
|
264
285
|
|
|
265
286
|
def each_pair?(value)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Services
|
|
5
|
+
class ResourceFieldGenerator
|
|
6
|
+
# The one definition of a generated field row: the attributes a
|
|
7
|
+
# ResourceField gets when a path creates rows for a model's columns at
|
|
8
|
+
# a level — ResourceFieldGenerator (the API's generate_fields,
|
|
9
|
+
# Resource.grant_full_access!, the console), the admin's presets and
|
|
10
|
+
# "Sync fields" (Admin::ColumnFields) and the Fields tab's bulk add
|
|
11
|
+
# (Admin::ChangeSetParts::NewFields). Three paths, one row
|
|
12
|
+
# (release_readiness/decision_parity_test.rb, part a):
|
|
13
|
+
#
|
|
14
|
+
# - the flags of the level (PermissionForge::FieldLevel), with the
|
|
15
|
+
# privacy default: under gdpr_defaults, on a granting set, a column
|
|
16
|
+
# that looks like personal data (PermissionForge::SensitiveColumns)
|
|
17
|
+
# created Read-only starts Masked. Editable is full access and masks
|
|
18
|
+
# nothing; Hidden stays Hidden (a privacy default never grants). A
|
|
19
|
+
# Muting Permission Set gets the level chosen: there a row takes
|
|
20
|
+
# access away, and the privacy default has nothing to add;
|
|
21
|
+
# - then Editable is Read-only on a Resource without Edit
|
|
22
|
+
# (FieldLevel.capped): chosen Editable, a personal-data field is
|
|
23
|
+
# Read-only there, not Masked;
|
|
24
|
+
# - Rails' timestamps at most Read-only on a granting set
|
|
25
|
+
# (PermissionForge::TimestampFields);
|
|
26
|
+
# - the recommended mask strategy of a sensitive column, whether or not
|
|
27
|
+
# the row is masked now, so masking it later uses the right one;
|
|
28
|
+
# 'full' otherwise;
|
|
29
|
+
# - the label (the name itself: never English made up from it — the
|
|
30
|
+
# admin shows the host's translation, or the name), active, editable.
|
|
31
|
+
#
|
|
32
|
+
# A blank name (a host whose extra_fields_method lists one) is no row,
|
|
33
|
+
# nor is a level that is none (FieldLevel.row?: Unchanged on a Muting
|
|
34
|
+
# Permission Set mutes nothing, so no path writes a row for it).
|
|
35
|
+
#
|
|
36
|
+
# @api private
|
|
37
|
+
class RowBuilder
|
|
38
|
+
# Granting levels the privacy default turns into Masked for a column
|
|
39
|
+
# that looks like personal data.
|
|
40
|
+
MASKED_BY_PRIVACY_DEFAULTS = %w[read].freeze
|
|
41
|
+
|
|
42
|
+
# @param resource [Resource] the rows' Resource (its name resolves the model)
|
|
43
|
+
# @param muting [Boolean, nil] the rows are a Muting Permission Set's
|
|
44
|
+
# (default: the Resource's set says)
|
|
45
|
+
# @param configuration [Configuration]
|
|
46
|
+
# @param sensitive [SensitiveColumns] the matcher (default: one on `configuration`)
|
|
47
|
+
def initialize(resource, muting: nil, configuration: PermissionForge.configuration, sensitive: nil)
|
|
48
|
+
@resource = resource
|
|
49
|
+
@muting = muting.nil? ? (resource.permission_set&.is_muting || false) : muting
|
|
50
|
+
@configuration = configuration
|
|
51
|
+
@sensitive = sensitive || PermissionForge::SensitiveColumns.new(configuration: configuration)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @return [Boolean] the rows are a Muting Permission Set's
|
|
55
|
+
def muting? = @muting
|
|
56
|
+
|
|
57
|
+
# @param column [String, Symbol] the field name (a column or a virtual field)
|
|
58
|
+
# @param level [String] a FieldLevel of the set's kind
|
|
59
|
+
# @return [Hash, nil] the row's attributes (without the Resource), or
|
|
60
|
+
# nil for a blank name or a level that is no row
|
|
61
|
+
# @raise [ArgumentError] a level the set's kind does not have
|
|
62
|
+
def attrs(column, level)
|
|
63
|
+
name = column.to_s
|
|
64
|
+
return if name.strip.empty?
|
|
65
|
+
return unless row_level?(level)
|
|
66
|
+
|
|
67
|
+
match = @sensitive.match(name)
|
|
68
|
+
level = PermissionForge::FieldLevel.capped(effective_level(level, match), resource_edit: @resource.edit_access, muting: @muting)
|
|
69
|
+
flags = PermissionForge::FieldLevel.flags_for(level, muting: @muting)
|
|
70
|
+
{ name: name, label: name, is_active: true, is_editable: true, mask_strategy: match&.strategy || 'full' }
|
|
71
|
+
.merge(flags)
|
|
72
|
+
.merge(timestamps.overrides(name, read: flags[:read_access]))
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def row_level?(level)
|
|
78
|
+
PermissionForge::FieldLevel.flags_for(level, muting: @muting) # raises for a level the kind lacks
|
|
79
|
+
PermissionForge::FieldLevel.row?(level, muting: @muting)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# The level after the privacy default.
|
|
83
|
+
def effective_level(level, match)
|
|
84
|
+
level = level.to_s
|
|
85
|
+
return level if @muting || match.nil? || !@configuration.gdpr_defaults
|
|
86
|
+
|
|
87
|
+
MASKED_BY_PRIVACY_DEFAULTS.include?(level) ? 'masked' : level
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def timestamps
|
|
91
|
+
@timestamps ||= PermissionForge::TimestampFields.new(PermissionForge::ModelLookup.model_for(@resource.name), muting: @muting)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -1,17 +1,56 @@
|
|
|
1
|
+
require_relative 'resource_field_generator/row_builder'
|
|
2
|
+
|
|
1
3
|
module PermissionForge
|
|
2
4
|
module Services
|
|
3
|
-
# Creates a ResourceField for every
|
|
4
|
-
#
|
|
5
|
+
# Creates a ResourceField for every field of a resource's model that has none: its columns and its
|
|
6
|
+
# virtual fields (PermissionForge::ManagedFields, the names the admin's presets and Sync write rows for).
|
|
5
7
|
#
|
|
6
8
|
# @api public
|
|
7
9
|
class ResourceFieldGenerator
|
|
10
|
+
# Every generated row is built by {RowBuilder}, the one definition the
|
|
11
|
+
# admin's presets, "Sync fields" and the Fields tab's bulk add share.
|
|
12
|
+
# With `grant_all` the caller asked for full access (Editable): every
|
|
13
|
+
# field readable and editable, unmasked — also the columns that look
|
|
14
|
+
# like personal data, which keep the recommended mask strategy for
|
|
15
|
+
# when someone masks them and are still reported as personal data
|
|
16
|
+
# (GdprFieldAuditor, the admin's privacy audit). Without it every
|
|
17
|
+
# field is Hidden; under `gdpr_defaults` the privacy default masks what
|
|
18
|
+
# is granted Read, and nothing is granted here. On a Muting Permission
|
|
19
|
+
# Set `grant_all` takes everything away (Hide); without it nothing is
|
|
20
|
+
# written — Unchanged is no row (FieldLevel.row?), as in the admin.
|
|
21
|
+
# On a Resource without Edit, Editable rows are Read-only
|
|
22
|
+
# (FieldLevel.capped). Rails' timestamps
|
|
23
|
+
# (created_at / updated_at) are at most read-only on a granting set
|
|
24
|
+
# (PermissionForge::TimestampFields).
|
|
25
|
+
#
|
|
8
26
|
# @param resource [Resource]
|
|
9
|
-
# @param grant_all [Boolean] grant read and edit on the generated fields
|
|
27
|
+
# @param grant_all [Boolean] grant read and edit, unmasked, on the generated fields
|
|
10
28
|
# @return [Array<ResourceField>] the fields created
|
|
11
29
|
def self.call(resource:, grant_all: false)
|
|
12
30
|
new(resource: resource, grant_all: grant_all).call
|
|
13
31
|
end
|
|
14
32
|
|
|
33
|
+
# The attributes of one generated row — the one definition every path
|
|
34
|
+
# that creates rows for a model's columns uses ({RowBuilder#attrs}).
|
|
35
|
+
#
|
|
36
|
+
# @param resource [Resource] its Permission Set says whether the row is a muting one
|
|
37
|
+
# @param column [String, Symbol] a column or virtual field name
|
|
38
|
+
# @param level [String] a FieldLevel of the set's kind
|
|
39
|
+
# @return [Hash, nil] the row's attributes, nil for a blank name
|
|
40
|
+
def self.build(resource:, column:, level:)
|
|
41
|
+
rows(resource).attrs(column, level)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# A builder for several rows of one Resource (the model and the matcher
|
|
45
|
+
# are resolved once). Keywords as {RowBuilder#initialize}: `muting:`
|
|
46
|
+
# (default: the Resource's set says), `configuration:`, `sensitive:`.
|
|
47
|
+
#
|
|
48
|
+
# @param resource [Resource]
|
|
49
|
+
# @return [RowBuilder]
|
|
50
|
+
def self.rows(resource, **)
|
|
51
|
+
RowBuilder.new(resource, **)
|
|
52
|
+
end
|
|
53
|
+
|
|
15
54
|
def initialize(resource:, grant_all: false)
|
|
16
55
|
@resource = resource
|
|
17
56
|
@grant_all = grant_all
|
|
@@ -19,9 +58,14 @@ module PermissionForge
|
|
|
19
58
|
|
|
20
59
|
# All or nothing, under one audit correlation id (an enclosing block's,
|
|
21
60
|
# if any): a field that cannot be saved leaves no field and no event.
|
|
61
|
+
# A Resource named after no model this app can load
|
|
62
|
+
# (PermissionForge::ModelLookup: `Custmer`, `Kernel`, a model without
|
|
63
|
+
# its table) has no columns: no fields, a log line — never an error
|
|
64
|
+
# (the API answers 422 before it gets here; `rake permission_forge:audit`
|
|
65
|
+
# lists the Resource under unknown_models).
|
|
22
66
|
def call
|
|
23
|
-
model_class = @resource.name
|
|
24
|
-
return
|
|
67
|
+
model_class = PermissionForge::ModelLookup.model_for(@resource.name)
|
|
68
|
+
return no_model unless model_class
|
|
25
69
|
|
|
26
70
|
fields = PermissionForge::Audit.correlate { PermissionForge::ResourceField.transaction { generate_fields(model_class) } }
|
|
27
71
|
PermissionForge.configuration.record_gdpr_pattern_checksum
|
|
@@ -42,51 +86,36 @@ module PermissionForge
|
|
|
42
86
|
|
|
43
87
|
private
|
|
44
88
|
|
|
89
|
+
def no_model
|
|
90
|
+
Rails.logger&.warn("[PermissionForge] #{@resource.name} names no model this app can load: no fields generated")
|
|
91
|
+
[]
|
|
92
|
+
end
|
|
93
|
+
|
|
45
94
|
def generate_fields(model_class)
|
|
46
95
|
existing_names = @resource.resource_fields.pluck(:name)
|
|
47
|
-
|
|
96
|
+
names = PermissionForge::ManagedFields.new(model_class).names
|
|
97
|
+
columns = self.class.without_blank_names(model_class, names) - existing_names
|
|
48
98
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
PermissionForge::ResourceField.create!(attrs)
|
|
99
|
+
rows = RowBuilder.new(@resource, muting: muting?)
|
|
100
|
+
columns.filter_map do |column_name|
|
|
101
|
+
attrs = rows.attrs(column_name, level)
|
|
102
|
+
PermissionForge::ResourceField.create!(attrs.merge(resource: @resource)) if attrs
|
|
53
103
|
end
|
|
54
104
|
end
|
|
55
105
|
|
|
56
|
-
def
|
|
57
|
-
|
|
58
|
-
resource: @resource,
|
|
59
|
-
name: column_name,
|
|
60
|
-
label: column_name.humanize,
|
|
61
|
-
is_active: true,
|
|
62
|
-
is_editable: true,
|
|
63
|
-
read_access: @grant_all,
|
|
64
|
-
edit_access: @grant_all
|
|
65
|
-
}
|
|
106
|
+
def muting?
|
|
107
|
+
@resource.permission_set&.is_muting || false
|
|
66
108
|
end
|
|
67
109
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
configuration = PermissionForge.configuration
|
|
78
|
-
return [] if configuration.grant_extra_fields_by_default?
|
|
79
|
-
|
|
80
|
-
self.class.without_blank_names(model_class, PermissionForge::VirtualFields.new(model_class, configuration).names)
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def apply_gdpr_defaults(attrs, column_name)
|
|
84
|
-
_pattern, strategy = PermissionForge.configuration.gdpr_field_patterns.find { |regex, _| column_name.match?(regex) }
|
|
85
|
-
return unless strategy
|
|
86
|
-
|
|
87
|
-
attrs[:read_access] = true
|
|
88
|
-
attrs[:mask_access] = true
|
|
89
|
-
attrs[:mask_strategy] = strategy
|
|
110
|
+
# The level generated rows get: full access or none; on a Muting
|
|
111
|
+
# Permission Set the level that takes everything away, or Unchanged,
|
|
112
|
+
# which the row builder writes no row for.
|
|
113
|
+
def level
|
|
114
|
+
if muting?
|
|
115
|
+
@grant_all ? 'hide' : 'leave'
|
|
116
|
+
else
|
|
117
|
+
@grant_all ? 'edit' : 'hidden'
|
|
118
|
+
end
|
|
90
119
|
end
|
|
91
120
|
end
|
|
92
121
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
module PermissionForge
|
|
2
2
|
module Services
|
|
3
|
-
# Creates a Resource with full CRUD access and its generated fields
|
|
3
|
+
# Creates a Resource with full CRUD access and its generated fields (read and
|
|
4
|
+
# edit, unmasked: ResourceFieldGenerator with grant_all), all or nothing.
|
|
4
5
|
#
|
|
5
6
|
# @api public
|
|
6
7
|
class ResourceWithFieldsBuilder
|