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
|
@@ -17,18 +17,86 @@ module PermissionForge
|
|
|
17
17
|
CRUD = %i[read_access create_access edit_access delete_access].freeze
|
|
18
18
|
RESOURCE_FLAGS = (CRUD + %i[view_all modify_all]).freeze
|
|
19
19
|
|
|
20
|
-
# A permission outcome with the sets that caused it.
|
|
21
|
-
|
|
20
|
+
# A permission outcome with the sets that caused it. `muted_on`: each
|
|
21
|
+
# blocking set => the Resource whose row blocks (a single-table-
|
|
22
|
+
# inheritance base or the resource itself; the row nearest the
|
|
23
|
+
# resource when the set has several), so a reason names the Resource
|
|
24
|
+
# the set acts through (LabelsHelper#pf_chain_set_name).
|
|
25
|
+
Outcome = Struct.new(:state, :granted_by, :blocked_by, :muted_on, keyword_init: true) do
|
|
26
|
+
# The outcome of the granting sets and the blocking ones (set => Resource).
|
|
27
|
+
def self.of(granted_by, muted_on)
|
|
28
|
+
state = if granted_by.empty? then :none
|
|
29
|
+
elsif muted_on.any? then :blocked
|
|
30
|
+
else :granted
|
|
31
|
+
end
|
|
32
|
+
new(state: state, granted_by: granted_by, blocked_by: muted_on.keys, muted_on: muted_on)
|
|
33
|
+
end
|
|
34
|
+
|
|
22
35
|
def granted? = state == :granted
|
|
23
36
|
def blocked? = state == :blocked
|
|
24
37
|
end
|
|
25
38
|
|
|
39
|
+
# What one Permission Set does to a field: its most open level (a
|
|
40
|
+
# granting set: masked, read or edit, with that row's mask strategy) or
|
|
41
|
+
# what it takes away (a muting set: mask, hide or no_edit) —
|
|
42
|
+
# Admin::FieldLevel names. `resource_name`: the Resource whose row
|
|
43
|
+
# does it — the deciding entry's for a grant, the muting row's (an STI
|
|
44
|
+
# base's or the resource's own) for a mute (field test round 8, B#38).
|
|
45
|
+
FieldSource = Struct.new(:permission_set, :level, :mask_strategy, :resource_name, keyword_init: true)
|
|
46
|
+
|
|
26
47
|
# `implicit` — no set configures the field for this outcome: it is a
|
|
27
48
|
# declared virtual field on the implicit read + edit grant
|
|
28
49
|
# (extra_fields_default_access = :grant).
|
|
50
|
+
# `grants` / `mutes` — the FieldSources behind granted_by / blocked_by, same order:
|
|
51
|
+
# the sets credited with what they did to the entry that decides.
|
|
52
|
+
# `decided_by` — for a single-table-inheritance subclass, the base
|
|
53
|
+
# Resource whose entry decides the field (ResourceChain: the strictest
|
|
54
|
+
# entry along the chain); nil when the resource's own entry does.
|
|
55
|
+
# `unreadable` — that entry does not grant Read, so it hides every field.
|
|
56
|
+
# The level a field shows when a mute decides it (FieldSource#level of a
|
|
57
|
+
# mute => Admin::FieldLevel granting level): hide => Hidden, mask =>
|
|
58
|
+
# Masked, no_edit => Read-only.
|
|
59
|
+
MUTED_LEVELS = { 'hide' => 'hidden', 'mask' => 'masked', 'no_edit' => 'read' }.freeze
|
|
60
|
+
|
|
29
61
|
FieldOutcome = Struct.new(:name, :level, :mask_strategy, :granted_by, :blocked_by, :blocked_level,
|
|
30
|
-
:capped, :unconfigured, :virtual, :variant_of, :implicit,
|
|
62
|
+
:capped, :unconfigured, :virtual, :variant_of, :implicit, :grants, :mutes,
|
|
63
|
+
:decided_by, :unreadable, keyword_init: true) do
|
|
31
64
|
def visible? = level != 'hidden'
|
|
65
|
+
# Hidden because a Muting Permission Set takes it away, not because no set grants it.
|
|
66
|
+
def hidden_by_muting? = level == 'hidden' && blocked_by.any?
|
|
67
|
+
|
|
68
|
+
# The mutes that decide the field's level: the strongest mute's level
|
|
69
|
+
# (blocked_level) when it is the level the field shows
|
|
70
|
+
# (MUTED_LEVELS), and every set credited at it; none otherwise — a
|
|
71
|
+
# mute that took editing away from a field a grant shows masked
|
|
72
|
+
# changed the entry but does not decide what it shows. The row and
|
|
73
|
+
# "Explain for" name these sets (review of round 8, D1/D2).
|
|
74
|
+
#
|
|
75
|
+
# @return [Array<FieldSource>]
|
|
76
|
+
def deciding_mutes
|
|
77
|
+
return [] unless blocked_level && MUTED_LEVELS[blocked_level] == level
|
|
78
|
+
|
|
79
|
+
mutes.select { |source| source.level == blocked_level }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Why the field is at its level: the one decision a person's
|
|
83
|
+
# Effective access row, its Why panel and the field drawer's
|
|
84
|
+
# "Explain for" all word (field test round 8, B#39) —
|
|
85
|
+
# :muted (#deciding_mutes: a Muting Permission Set decides), :unconfigured, :granted,
|
|
86
|
+
# :implicit (a virtual field's default), :unreadable (the deciding
|
|
87
|
+
# entry does not grant Read), :inherited (an STI base's entry
|
|
88
|
+
# decides and shows it to nobody) or :none (no set shows it).
|
|
89
|
+
#
|
|
90
|
+
# @return [Symbol]
|
|
91
|
+
def reason
|
|
92
|
+
return :muted if deciding_mutes.any?
|
|
93
|
+
return :unconfigured if unconfigured
|
|
94
|
+
return :granted if granted_by.any?
|
|
95
|
+
return :implicit if implicit
|
|
96
|
+
return :unreadable if unreadable
|
|
97
|
+
|
|
98
|
+
decided_by ? :inherited : :none
|
|
99
|
+
end
|
|
32
100
|
end
|
|
33
101
|
|
|
34
102
|
RuleSource = Struct.new(:rule, :permission_set, keyword_init: true)
|
|
@@ -39,8 +107,25 @@ module PermissionForge
|
|
|
39
107
|
end
|
|
40
108
|
|
|
41
109
|
# One resource name with its [permission_set, resource] pairs from the
|
|
42
|
-
# granting and muting sets,
|
|
43
|
-
|
|
110
|
+
# granting and muting sets, the resulting resource flag outcomes and
|
|
111
|
+
# the record rules that apply (its own entry's and, for a
|
|
112
|
+
# single-table-inheritance subclass, its bases' — ResourceChain.rules).
|
|
113
|
+
ResourceSources = Struct.new(:name, :grants, :mutes, :flags, :rules, keyword_init: true) do
|
|
114
|
+
# View All / Modify All on this entry skips its own rules (ResourceChain.rules).
|
|
115
|
+
def bypassed? = flags[:view_all].granted? || flags[:modify_all].granted?
|
|
116
|
+
|
|
117
|
+
# :all when View All is granted and no rule applies (a base entry's
|
|
118
|
+
# may, past the subclass's own bypass); :rules otherwise.
|
|
119
|
+
def records
|
|
120
|
+
return :none unless flags[:read_access].granted?
|
|
121
|
+
|
|
122
|
+
flags[:view_all].granted? && rules.empty? ? :all : :rules
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def outcome(fields:, actions:)
|
|
126
|
+
ResourceOutcome.new(name: name, flags: flags, records: records, rules: rules, fields: fields, actions: actions)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
44
129
|
|
|
45
130
|
attr_reader :live_sets, :conditional_sets
|
|
46
131
|
|
|
@@ -59,17 +144,20 @@ module PermissionForge
|
|
|
59
144
|
granting, muting = @live_sets.partition(&:granting?)
|
|
60
145
|
|
|
61
146
|
names = granting.flat_map { |ps| active_resources(ps).map(&:name) }.uniq
|
|
62
|
-
names.map { |name|
|
|
147
|
+
names.map { |name| outcome_for_name(name, granting, muting) }
|
|
63
148
|
end
|
|
64
149
|
|
|
65
150
|
private
|
|
66
151
|
|
|
67
152
|
# Same selection as PermissionCacheAccessor#active_non_expired_permission_sets
|
|
68
|
-
# minus the request-context filter
|
|
153
|
+
# minus the request-context filter — a granting set whose condition
|
|
154
|
+
# nobody can judge never passes it, so it is left out
|
|
155
|
+
# (ActivationConditions#never_grants?).
|
|
69
156
|
def select_live_sets
|
|
70
157
|
ids = PermissionForge::PermissionSetUser.where(user_id: @user.id).active_assignments.select(:permission_set_id)
|
|
71
|
-
sets = PermissionForge::PermissionSet.active.where(id: ids).
|
|
72
|
-
.includes(resources: %i[resource_fields resource_actions permission_set_rules])
|
|
158
|
+
sets = PermissionForge::PermissionSet.active.where(id: ids).in_hash_order
|
|
159
|
+
.includes(resources: %i[resource_fields resource_actions permission_set_rules])
|
|
160
|
+
.reject(&:never_grants?)
|
|
73
161
|
# Same row order as the hash builder (a variant tie goes to the older row).
|
|
74
162
|
sets.each { |permission_set| PermissionForge::Services::PermissionHashBuilder.sort_rows(permission_set) }
|
|
75
163
|
end
|
|
@@ -79,13 +167,26 @@ module PermissionForge
|
|
|
79
167
|
end
|
|
80
168
|
|
|
81
169
|
# [permission_set, resource] for every active row of the sets named by
|
|
82
|
-
# one of `names`, in set order then
|
|
170
|
+
# one of `names`, in set order, then in the order of `names` (a muting
|
|
171
|
+
# set's row nearest the resource first), then row order.
|
|
83
172
|
def pairs_for(permission_sets, names)
|
|
84
173
|
permission_sets.flat_map do |ps|
|
|
85
|
-
active_resources(ps)
|
|
174
|
+
ChainEntries.in_chain_order(active_resources(ps), names).map { |resource| [ps, resource] }
|
|
86
175
|
end
|
|
87
176
|
end
|
|
88
177
|
|
|
178
|
+
# The outcome for one resource name: its sources, the rules along its
|
|
179
|
+
# chain (ResourceChain.names_for), then the fields — composed with the
|
|
180
|
+
# entries of its bases the person has (ResourceChain.fields) — and
|
|
181
|
+
# actions.
|
|
182
|
+
def outcome_for_name(name, granting, muting)
|
|
183
|
+
chain = PermissionForge::ResourceChain.names_for(name).map(&:to_s)
|
|
184
|
+
sources = sources_for(name, granting, muting)
|
|
185
|
+
sources.rules = rules_along(chain, granting, muting)
|
|
186
|
+
bases = chain.drop(1).map { |base| sources_for(base, granting, muting) }.select { |base| base.grants.any? }
|
|
187
|
+
build_resource(sources, bases)
|
|
188
|
+
end
|
|
189
|
+
|
|
89
190
|
# A granting set's row for the resource; a muting set's rows for the
|
|
90
191
|
# resource and for the STI bases of the model it names — the engine's
|
|
91
192
|
# cascade (PermissionHashBuilder#apply_muting_permission_set): a muted
|
|
@@ -93,44 +194,45 @@ module PermissionForge
|
|
|
93
194
|
def sources_for(name, granting, muting)
|
|
94
195
|
grants = pairs_for(granting, [name])
|
|
95
196
|
mutes = pairs_for(muting, PermissionForge::ResourceChain.names_for(name).map(&:to_s))
|
|
96
|
-
flags = RESOURCE_FLAGS.index_with { |flag|
|
|
197
|
+
flags = RESOURCE_FLAGS.index_with { |flag| flag_outcome(grants, mutes, flag) }
|
|
97
198
|
ResourceSources.new(name: name, grants: grants, mutes: mutes, flags: flags)
|
|
98
199
|
end
|
|
99
200
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
actions: action_outcomes(grants, sources.mutes)
|
|
108
|
-
)
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
# Granted by every granting set whose resource matches; muting sets only
|
|
112
|
-
# block what some set granted.
|
|
113
|
-
def outcome_for(grants, mutes, &)
|
|
114
|
-
granted_by = sets_matching(grants, &)
|
|
115
|
-
blocked_by = granted_by.any? ? sets_matching(mutes, &) : []
|
|
116
|
-
Outcome.new(state: outcome_state(granted_by, blocked_by), granted_by: granted_by, blocked_by: blocked_by)
|
|
201
|
+
# The rules that apply to records of `chain.first` (ResourceChain.rules):
|
|
202
|
+
# of every entry along the chain without a bypass of its own.
|
|
203
|
+
def rules_along(chain, granting, muting)
|
|
204
|
+
chain.flat_map do |name|
|
|
205
|
+
sources = sources_for(name, granting, muting)
|
|
206
|
+
sources.bypassed? ? [] : rules_for(sources.grants)
|
|
207
|
+
end
|
|
117
208
|
end
|
|
118
209
|
|
|
119
|
-
#
|
|
120
|
-
|
|
121
|
-
|
|
210
|
+
# `bases`: the ResourceSources of the base entries the person has
|
|
211
|
+
# (a granting row names them — the engine's entries), nearest first.
|
|
212
|
+
def build_resource(sources, bases)
|
|
213
|
+
fields = EffectiveFields.new(sources, configuration: @configuration, catalog: @catalog, bases: bases).call
|
|
214
|
+
sources.outcome(fields: fields, actions: action_outcomes(sources))
|
|
122
215
|
end
|
|
123
216
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
217
|
+
# A resource flag: granted by the granting rows that grant it as the
|
|
218
|
+
# engine reads them (PermissionHashBuilder.granted_flags — edit and
|
|
219
|
+
# delete only with read, Modify All only with View All), blocked by the
|
|
220
|
+
# muting rows that carry it.
|
|
221
|
+
def flag_outcome(grants, mutes, flag)
|
|
222
|
+
outcome_for(grants, mutes) do |resource, granting|
|
|
223
|
+
granting ? PermissionForge::Services::PermissionHashBuilder.granted_flags(resource).fetch(flag) : resource.public_send(flag) == true
|
|
224
|
+
end
|
|
128
225
|
end
|
|
129
226
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
227
|
+
# Granted by every granting set whose resource matches; muting sets only
|
|
228
|
+
# block what some set granted, each set once (a muting set may reach an
|
|
229
|
+
# entry through two rows: its first that blocks is named, pairs_for
|
|
230
|
+
# puts the one nearest the resource first). The block gets the row and
|
|
231
|
+
# whether it is a granting one.
|
|
232
|
+
def outcome_for(grants, mutes)
|
|
233
|
+
granted_by = grants.select { |_, resource| yield resource, true }.map(&:first).uniq
|
|
234
|
+
blocking = granted_by.any? ? mutes.select { |_, resource| yield resource, false } : []
|
|
235
|
+
Outcome.of(granted_by, blocking.group_by(&:first).transform_values { |rows| rows.first.last.name })
|
|
134
236
|
end
|
|
135
237
|
|
|
136
238
|
def rules_for(grants)
|
|
@@ -141,17 +243,14 @@ module PermissionForge
|
|
|
141
243
|
|
|
142
244
|
# --- Actions ----------------------------------------------------------
|
|
143
245
|
|
|
144
|
-
def action_outcomes(
|
|
145
|
-
|
|
146
|
-
names.
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
def active_actions(resource)
|
|
150
|
-
resource.resource_actions.select(&:is_active)
|
|
246
|
+
def action_outcomes(sources)
|
|
247
|
+
grants = sources.grants
|
|
248
|
+
names = grants.flat_map { |_, r| r.resource_actions.select(&:is_active).map(&:name) }.uniq
|
|
249
|
+
names.map { |action| [action, outcome_for(grants, sources.mutes) { |r, _granting| permitted?(r, action) }] }
|
|
151
250
|
end
|
|
152
251
|
|
|
153
252
|
def permitted?(resource, action)
|
|
154
|
-
|
|
253
|
+
resource.resource_actions.any? { |row| row.is_active && row.is_permitted && row.name == action }
|
|
155
254
|
end
|
|
156
255
|
end
|
|
157
256
|
end
|
|
@@ -10,6 +10,12 @@ module PermissionForge
|
|
|
10
10
|
# Extra fields follow the engine: after every granting set is merged, each
|
|
11
11
|
# declared name that nothing configured gets the implicit entry
|
|
12
12
|
# (ExtraFieldDefaults). A row in any set wins, whatever the set order.
|
|
13
|
+
#
|
|
14
|
+
# The explanation comes from the entry that decides (EX): each set is
|
|
15
|
+
# credited with what it did to the final entry (FieldProvenance). A
|
|
16
|
+
# subclass's field is composed along its single-table-inheritance chain
|
|
17
|
+
# as ResourceChain composes it (`bases`, FieldChain): the strictest entry
|
|
18
|
+
# decides, and the outcome says which (FieldOutcome#decided_by).
|
|
13
19
|
class EffectiveFields
|
|
14
20
|
FieldOutcome = EffectiveAccess::FieldOutcome
|
|
15
21
|
|
|
@@ -18,56 +24,91 @@ module PermissionForge
|
|
|
18
24
|
UNCONFIGURED_LEVELS = { 'pass_through' => 'read', 'warn' => 'read', 'mask_all' => 'masked' }.freeze
|
|
19
25
|
|
|
20
26
|
# @param sources [EffectiveAccess::ResourceSources]
|
|
21
|
-
|
|
27
|
+
# @param bases [Array<EffectiveAccess::ResourceSources>] the entries the
|
|
28
|
+
# user has for the model's STI bases, nearest first (ResourceChain)
|
|
29
|
+
def initialize(sources, configuration:, catalog:, bases: [])
|
|
22
30
|
@sources = sources
|
|
23
31
|
@configuration = configuration
|
|
24
32
|
@catalog = catalog
|
|
25
|
-
|
|
33
|
+
@bases = bases
|
|
34
|
+
# entry: { read_access:, edit_access:, mask_access:, mask_strategy:, sources: [[set, level, strategy, resource]],
|
|
35
|
+
# variant_of:, before: (the flags before muting), mutes: [[set, level, resource]] }
|
|
36
|
+
# (resource: the name of the Resource whose row it is)
|
|
26
37
|
@entries = {}
|
|
27
38
|
end
|
|
28
39
|
|
|
29
40
|
# @return [Array<EffectiveAccess::FieldOutcome>]
|
|
30
41
|
def call
|
|
42
|
+
chain = FieldChain.new(chain_tables)
|
|
43
|
+
model = @catalog.find(@sources.name)
|
|
44
|
+
(chain.names + Array(model&.managed_field_names)).uniq.map do |field|
|
|
45
|
+
composed_outcome(chain, field, model.present? && model.virtual_field?(field))
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# The resource's own field entries, merged and muted as the engine does.
|
|
50
|
+
#
|
|
51
|
+
# @return [Hash{String => Hash}]
|
|
52
|
+
def entries
|
|
53
|
+
return @entries if @merged
|
|
54
|
+
|
|
55
|
+
@merged = true
|
|
31
56
|
merge_granting_fields
|
|
32
57
|
apply_muting_fields
|
|
58
|
+
@entries
|
|
59
|
+
end
|
|
33
60
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
# The resource's FieldChain::Table, then each base's along its chain.
|
|
64
|
+
def chain_tables
|
|
65
|
+
[FieldChain::Table.new(@sources, entries, nil)] + @bases.map do |base|
|
|
66
|
+
FieldChain::Table.new(base, self.class.new(base, configuration: @configuration, catalog: @catalog).entries, base.name)
|
|
37
67
|
end
|
|
38
68
|
end
|
|
39
69
|
|
|
40
|
-
|
|
70
|
+
def composed_outcome(chain, name, virtual)
|
|
71
|
+
attributes = chain.attributes(name, virtual: virtual, editable: @sources.flags[:edit_access].granted?)
|
|
72
|
+
attributes ? FieldOutcome.new(name: name, unconfigured: false, **attributes) : unconfigured_outcome(name, virtual)
|
|
73
|
+
end
|
|
41
74
|
|
|
42
75
|
def active_fields(resource)
|
|
43
76
|
resource.resource_fields.select(&:is_active)
|
|
44
77
|
end
|
|
45
78
|
|
|
79
|
+
# The engine's variants (FieldVariants): only names the model has.
|
|
46
80
|
def variants_of(name)
|
|
47
|
-
|
|
81
|
+
variants.call(name)
|
|
48
82
|
end
|
|
49
83
|
|
|
84
|
+
# The engine's own targets (FieldPermissionMerger.targets_of): a
|
|
85
|
+
# resource's rows under their own names, then the derived variants of
|
|
86
|
+
# the names no row of the resource has.
|
|
50
87
|
def merge_granting_fields
|
|
51
88
|
@sources.grants.each do |ps, resource|
|
|
52
|
-
active_fields(resource).each
|
|
89
|
+
Services::FieldPermissionMerger.targets_of(active_fields(resource), variants).each do |name, field, variant_of|
|
|
90
|
+
merge_into(entry_for(name, variant_of), field, ps)
|
|
91
|
+
end
|
|
53
92
|
end
|
|
54
93
|
implicit_names.each { |name| @entries[name] ||= implicit_entry }
|
|
55
94
|
end
|
|
56
95
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
96
|
+
# An explicit row for the name (variant_of nil) makes the entry a field
|
|
97
|
+
# of its own, whichever set wrote the derived entry first.
|
|
98
|
+
def entry_for(key, variant_of)
|
|
99
|
+
entry = (@entries[key] ||= { sources: [], variant_of: variant_of })
|
|
100
|
+
entry[:variant_of] = nil if variant_of.nil?
|
|
101
|
+
entry
|
|
61
102
|
end
|
|
62
103
|
|
|
63
|
-
def
|
|
64
|
-
@
|
|
104
|
+
def variants
|
|
105
|
+
@variants ||= FieldVariants.new(@sources.name, @configuration)
|
|
65
106
|
end
|
|
66
107
|
|
|
67
108
|
# The engine's own merge (FieldPermissionMerger.merge_row), plus provenance.
|
|
68
109
|
def merge_into(entry, field, permission_set)
|
|
69
110
|
Services::FieldPermissionMerger.merge_row(entry, field)
|
|
70
|
-
entry[:sources] << [permission_set, FieldLevel.level_of(field, muting: false), field.mask_strategy]
|
|
111
|
+
entry[:sources] << [permission_set, FieldLevel.level_of(field, muting: false), field.mask_strategy, @sources.name]
|
|
71
112
|
end
|
|
72
113
|
|
|
73
114
|
# Declared names that get the implicit read + edit entry; none under
|
|
@@ -75,7 +116,7 @@ module PermissionForge
|
|
|
75
116
|
# unconfigured virtual fields instead). Read for any constant, like the
|
|
76
117
|
# engine does, not only for the models the catalog lists.
|
|
77
118
|
def implicit_names
|
|
78
|
-
PermissionForge::VirtualFields.new(@sources.name
|
|
119
|
+
PermissionForge::VirtualFields.new(PermissionForge::ModelLookup.model_for(@sources.name), @configuration).implicit
|
|
79
120
|
end
|
|
80
121
|
|
|
81
122
|
# The engine's entry for the resource flags as the granting sets merged
|
|
@@ -86,86 +127,53 @@ module PermissionForge
|
|
|
86
127
|
end
|
|
87
128
|
|
|
88
129
|
def apply_muting_fields
|
|
130
|
+
@entries.each_value { |entry| entry[:before] = entry.slice(:read_access, :edit_access, :mask_access) }
|
|
89
131
|
@sources.mutes.each do |ps, resource|
|
|
90
|
-
active_fields(resource).each { |field| apply_muting_field(ps, field) }
|
|
91
|
-
hide_every_field(ps) if resource.read_access
|
|
132
|
+
active_fields(resource).each { |field| apply_muting_field(ps, field, resource.name) }
|
|
133
|
+
hide_every_field(ps, resource.name) if resource.read_access
|
|
92
134
|
end
|
|
93
135
|
end
|
|
94
136
|
|
|
95
137
|
# A muting set that revokes the resource's read hides every field, as
|
|
96
138
|
# the engine does (PermissionHashBuilder#mute_fields_of_resource). One
|
|
97
|
-
# that revokes its edit caps every field to read:
|
|
139
|
+
# that revokes its edit caps every field to read: FieldProvenance does that
|
|
98
140
|
# from the resource's edit outcome.
|
|
99
|
-
def hide_every_field(permission_set)
|
|
100
|
-
@entries.each_value { |entry| hide_entry(entry, permission_set) }
|
|
141
|
+
def hide_every_field(permission_set, resource_name)
|
|
142
|
+
@entries.each_value { |entry| hide_entry(entry, permission_set, resource_name) }
|
|
101
143
|
end
|
|
102
144
|
|
|
103
|
-
def hide_entry(entry, permission_set)
|
|
145
|
+
def hide_entry(entry, permission_set, resource_name)
|
|
104
146
|
entry[:read_access] = false
|
|
105
147
|
entry[:edit_access] = false
|
|
106
|
-
(entry[:mutes] ||= []) << [permission_set, 'hide']
|
|
148
|
+
(entry[:mutes] ||= []) << [permission_set, 'hide', resource_name]
|
|
107
149
|
end
|
|
108
150
|
|
|
109
|
-
def apply_muting_field(permission_set, field)
|
|
151
|
+
def apply_muting_field(permission_set, field, resource_name)
|
|
110
152
|
name = field.name
|
|
111
153
|
([name] + variants_of(name)).each do |key|
|
|
112
154
|
entry = @entries[key]
|
|
113
|
-
mute_field(entry, field, permission_set) if entry
|
|
155
|
+
mute_field(entry, field, [permission_set, resource_name]) if entry
|
|
114
156
|
end
|
|
115
157
|
end
|
|
116
158
|
|
|
117
159
|
# The engine's own muting of a row (PermissionHashBuilder
|
|
118
160
|
# #mute_single_field: a row that mutes read mutes edit too), plus
|
|
119
|
-
# provenance.
|
|
120
|
-
def mute_field(entry, field,
|
|
121
|
-
level = FieldLevel.
|
|
161
|
+
# provenance (`muting`: [set, the name of the Resource of the row]).
|
|
162
|
+
def mute_field(entry, field, muting)
|
|
163
|
+
level = FieldLevel.muting_provenance(field)
|
|
122
164
|
return if level == 'leave'
|
|
123
165
|
|
|
124
166
|
entry[:read_access] = false if field.read_access
|
|
125
167
|
entry[:edit_access] = false if field.read_access || field.edit_access
|
|
126
168
|
entry[:mask_access] = true if field.mask_access
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
def field_outcome(name, virtual)
|
|
131
|
-
entry = @entries[name]
|
|
132
|
-
return unconfigured_outcome(name, virtual) if entry.nil?
|
|
133
|
-
|
|
134
|
-
implicit = entry[:extra].present?
|
|
135
|
-
FieldOutcome.new(
|
|
136
|
-
name: name, mask_strategy: entry[:mask_strategy], granted_by: granting_sources(entry),
|
|
137
|
-
unconfigured: false, virtual: virtual || implicit, implicit: implicit, variant_of: entry[:variant_of],
|
|
138
|
-
**level_attrs(entry), **muting_attrs(entry)
|
|
139
|
-
)
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
# Editable fields are capped to Read when the resource's Edit is not granted.
|
|
143
|
-
def level_attrs(entry)
|
|
144
|
-
level = FieldLevel.effective_level(entry)
|
|
145
|
-
capped = level == 'edit' && !@sources.flags[:edit_access].granted?
|
|
146
|
-
{ level: capped ? 'read' : level, capped: capped }
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
def muting_attrs(entry)
|
|
150
|
-
mutes = entry[:mutes] || []
|
|
151
|
-
{ blocked_by: mutes.map(&:first).uniq, blocked_level: mutes.last&.last }
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
# Sets that make the field readable, most open first. A set that lists
|
|
155
|
-
# the field as Hidden grants nothing and, like the engine, has no say in
|
|
156
|
-
# the mask. An implicit entry exists only when no set configured the
|
|
157
|
-
# field, so no set is credited for it: the level comes from the
|
|
158
|
-
# virtual-field default.
|
|
159
|
-
def granting_sources(entry)
|
|
160
|
-
return [] if entry[:extra]
|
|
161
|
-
|
|
162
|
-
entry[:sources].select { |_, lvl, _| FieldLevel.rank(lvl) >= 1 }
|
|
163
|
-
.sort_by { |_, lvl, _| -FieldLevel.rank(lvl) }.map(&:first).uniq
|
|
169
|
+
permission_set, resource_name = muting
|
|
170
|
+
(entry[:mutes] ||= []) << [permission_set, level, resource_name]
|
|
164
171
|
end
|
|
165
172
|
|
|
166
173
|
def unconfigured_outcome(name, virtual)
|
|
167
|
-
FieldOutcome.new(name: name, level: unconfigured_level, mask_strategy: 'full', granted_by: [],
|
|
168
|
-
blocked_by: [], capped: false, unconfigured: true, virtual: virtual, implicit: false
|
|
174
|
+
FieldOutcome.new(name: name, level: unconfigured_level, mask_strategy: 'full', granted_by: [], grants: [],
|
|
175
|
+
blocked_by: [], mutes: [], capped: false, unconfigured: true, virtual: virtual, implicit: false,
|
|
176
|
+
decided_by: nil, unreadable: false)
|
|
169
177
|
end
|
|
170
178
|
|
|
171
179
|
def unconfigured_level
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
# A model's refusals as sentences in the page's language ("Name already
|
|
6
|
+
# exists in this permission set"), from the gem's own keys: Rails'
|
|
7
|
+
# `full_messages` reads `errors.format` and `human_attribute_name`,
|
|
8
|
+
# English in a host without rails-i18n. The attribute is named by
|
|
9
|
+
# `permission_forge.validations.attributes.<attribute>`, else by the
|
|
10
|
+
# attribute's stored name verbatim; the message is the validator's
|
|
11
|
+
# (every validator of the gem's models has a `permission_forge.validations.*` key).
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# ErrorSentence.all(record.errors) # => ["Name already exists in this permission set"]
|
|
15
|
+
#
|
|
16
|
+
# @api private
|
|
17
|
+
module ErrorSentence
|
|
18
|
+
SCOPE = 'permission_forge.validations'
|
|
19
|
+
|
|
20
|
+
# @param errors [ActiveModel::Errors]
|
|
21
|
+
# @return [Array<String>]
|
|
22
|
+
def self.all(errors)
|
|
23
|
+
errors.map { |error| sentence(error.attribute, error.message) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @param errors [ActiveModel::Errors]
|
|
27
|
+
# @return [String, nil]
|
|
28
|
+
def self.first(errors)
|
|
29
|
+
all(errors).first
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @param attribute [Symbol, String]
|
|
33
|
+
# @param message [String] the validator's message
|
|
34
|
+
# @return [String]
|
|
35
|
+
def self.sentence(attribute, message)
|
|
36
|
+
return message.to_s if attribute.to_s == 'base'
|
|
37
|
+
|
|
38
|
+
I18n.t("#{SCOPE}.full_message", attribute: attribute_label(attribute), message: message)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @return [String] the attribute's label, else its name as stored
|
|
42
|
+
def self.attribute_label(attribute)
|
|
43
|
+
I18n.t("#{SCOPE}.attributes.#{attribute}", default: attribute.to_s)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -9,6 +9,9 @@ module PermissionForge
|
|
|
9
9
|
DAYS = { '1d' => 1, '1w' => 7, '30d' => 30 }.freeze
|
|
10
10
|
KEYS = %w[1d 1w 30d eom custom never].freeze
|
|
11
11
|
CHANGE_KEYS = %w[1w 30d eom never].freeze
|
|
12
|
+
# Home's "Extend 30 days": 30 days past the current end date (or today).
|
|
13
|
+
EXTEND_KEY = 'extend30'
|
|
14
|
+
EXTEND_DAYS = 30
|
|
12
15
|
|
|
13
16
|
class InvalidDate < StandardError; end
|
|
14
17
|
|
|
@@ -28,6 +31,20 @@ module PermissionForge
|
|
|
28
31
|
date_for(key, today: today, custom: custom)&.in_time_zone&.end_of_day
|
|
29
32
|
end
|
|
30
33
|
|
|
34
|
+
# The end date a change of an assignment's end date gives: a CHANGE_KEYS
|
|
35
|
+
# preset, or EXTEND_KEY (EXTEND_DAYS past `from`, or past today when
|
|
36
|
+
# `from` is earlier or nil). :invalid for any other key.
|
|
37
|
+
#
|
|
38
|
+
# @param from [Time, nil] the end date the assignment has
|
|
39
|
+
# @return [Time, nil, :invalid]
|
|
40
|
+
def change_to(key, from:, today: Time.zone.today)
|
|
41
|
+
key = key.to_s
|
|
42
|
+
return ([from&.to_date, today].compact.max + EXTEND_DAYS).in_time_zone.end_of_day if key == EXTEND_KEY
|
|
43
|
+
return :invalid unless CHANGE_KEYS.include?(key)
|
|
44
|
+
|
|
45
|
+
expires_at_for(key, today: today)
|
|
46
|
+
end
|
|
47
|
+
|
|
31
48
|
def parse_custom(value, today)
|
|
32
49
|
raise InvalidDate, :missing if value.blank?
|
|
33
50
|
|
|
@@ -7,7 +7,10 @@ module PermissionForge
|
|
|
7
7
|
# Permission Set, when and by whom it was granted, when it ends, when
|
|
8
8
|
# and by whom it was last reviewed, and the reason of its last change.
|
|
9
9
|
# Granted by is the actor of the assignment.granted event when the
|
|
10
|
-
# audit log has one
|
|
10
|
+
# audit log has one — named as the admin names it (Admin::ActorLabel:
|
|
11
|
+
# a code such as an API client nobody named in the export's language;
|
|
12
|
+
# the actor's kind and id stay as stored, and the events export keeps
|
|
13
|
+
# the stored label) — else the created_by column.
|
|
11
14
|
#
|
|
12
15
|
# The rows are read while the file is written (batches of BATCH_SIZE,
|
|
13
16
|
# the way the events export does), never all at once. `row_count` in the
|
|
@@ -59,7 +62,7 @@ module PermissionForge
|
|
|
59
62
|
|
|
60
63
|
def each_batch(assignments)
|
|
61
64
|
assignments.find_in_batches(batch_size: @batch_size) do |batch|
|
|
62
|
-
granted = LastChanges.
|
|
65
|
+
granted = LastChanges.creations(batch)
|
|
63
66
|
people = people_by_id(batch)
|
|
64
67
|
batch.each { |assignment| yield row(assignment, granted[assignment], people) }
|
|
65
68
|
end
|
|
@@ -87,7 +90,7 @@ module PermissionForge
|
|
|
87
90
|
def granted_by(assignment, event, people)
|
|
88
91
|
if event
|
|
89
92
|
return { 'granted_by_type' => event.actor_type, 'granted_by_id' => event.actor_id,
|
|
90
|
-
'granted_by' => event
|
|
93
|
+
'granted_by' => ActorLabel.for(event) }
|
|
91
94
|
end
|
|
92
95
|
|
|
93
96
|
id = assignment.try(:created_by_id)
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
module PermissionForge
|
|
4
4
|
module Admin
|
|
5
|
-
# Every Permission Set containing a resource
|
|
6
|
-
#
|
|
7
|
-
#
|
|
5
|
+
# Every Permission Set containing a resource — or, for a
|
|
6
|
+
# single-table-inheritance subclass, one of the bases its records answer
|
|
7
|
+
# to (ResourceChain: a base's field row reaches the subclass) — with one
|
|
8
|
+
# field's row there (granting sets first, then by name) and the set's
|
|
9
|
+
# people count. Backs the field drawer's "In other sets" list and "Who can see …?".
|
|
8
10
|
class FieldAcrossSets
|
|
9
11
|
# @return [Array<Hash>] { resource:, set:, field: (ResourceField or nil), people:, in_effect: }
|
|
10
12
|
# in_effect is whether the row (or, without a row, the resource) counts
|
|
@@ -27,7 +29,8 @@ module PermissionForge
|
|
|
27
29
|
private
|
|
28
30
|
|
|
29
31
|
def resources
|
|
30
|
-
PermissionForge::
|
|
32
|
+
names = PermissionForge::ResourceChain.names_for(@resource_name).map(&:to_s)
|
|
33
|
+
PermissionForge::Resource.where(name: names).includes(:resource_fields, permission_set: :resources)
|
|
31
34
|
.sort_by { |resource| sort_key(resource.permission_set) }
|
|
32
35
|
end
|
|
33
36
|
|