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
|
@@ -4,7 +4,10 @@ module PermissionForge
|
|
|
4
4
|
module Admin
|
|
5
5
|
module ChangeSetParts
|
|
6
6
|
# New fields: columns without a ResourceField row, picked one by one or
|
|
7
|
-
# through a bulk selection.
|
|
7
|
+
# through a bulk selection. Only the model's managed fields
|
|
8
|
+
# (PermissionForge::ManagedFields — the names the presets, Sync and the
|
|
9
|
+
# generator write rows for) can be added: a posted `id`, an ignored
|
|
10
|
+
# field or a name the model does not have is no row.
|
|
8
11
|
class NewFields < Base
|
|
9
12
|
def call
|
|
10
13
|
explicit = section('new_fields').flat_map { |resource_id, columns| explicit_new_fields(resource_id, columns) }
|
|
@@ -20,7 +23,7 @@ module PermissionForge
|
|
|
20
23
|
existing = existing_names(resource)
|
|
21
24
|
columns.filter_map do |column, attrs|
|
|
22
25
|
name = column.to_s
|
|
23
|
-
next if existing.include?(name) || !attrs.is_a?(Hash)
|
|
26
|
+
next if existing.include?(name) || !attrs.is_a?(Hash) || !addable?(resource, name)
|
|
24
27
|
|
|
25
28
|
build_new_field(resource, name, attrs['level'].to_s)
|
|
26
29
|
end
|
|
@@ -36,7 +39,21 @@ module PermissionForge
|
|
|
36
39
|
|
|
37
40
|
# Bulk-selected columns that have neither a field nor an explicit pick.
|
|
38
41
|
def bulk_columns(resource)
|
|
39
|
-
Array(bulk['keys']).map(&:to_s).uniq - existing_names(resource) - explicit_columns(resource)
|
|
42
|
+
(Array(bulk['keys']).map(&:to_s).uniq & addable_names(resource)) - existing_names(resource) - explicit_columns(resource)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def addable?(resource, name)
|
|
46
|
+
addable_names(resource).include?(name)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# The model's managed fields; none when the Resource names no model
|
|
50
|
+
# this app can load.
|
|
51
|
+
def addable_names(resource)
|
|
52
|
+
@addable_names ||= {}
|
|
53
|
+
@addable_names[resource.id] ||= begin
|
|
54
|
+
model = PermissionForge::ModelLookup.model_for(resource.name)
|
|
55
|
+
model ? PermissionForge::ManagedFields.new(model).names : []
|
|
56
|
+
end
|
|
40
57
|
end
|
|
41
58
|
|
|
42
59
|
def existing_names(resource)
|
|
@@ -47,15 +64,25 @@ module PermissionForge
|
|
|
47
64
|
(params.dig('new_fields', resource.id.to_s) || {}).keys.map(&:to_s)
|
|
48
65
|
end
|
|
49
66
|
|
|
67
|
+
# The row comes from the one row builder (ResourceFieldGenerator::
|
|
68
|
+
# RowBuilder: privacy defaults, timestamps, the recommended strategy),
|
|
69
|
+
# so the change records the level the row really gets — Masked for a
|
|
70
|
+
# sensitive column added Read-only under gdpr_defaults, Read-only for
|
|
71
|
+
# a timestamp added Editable. A blank name is no row.
|
|
50
72
|
def build_new_field(resource, column, level)
|
|
51
|
-
return nil unless valid_level?(level) && level
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
field
|
|
57
|
-
change(kind: :new_field, record: field, attribute: 'level', from: nil,
|
|
58
|
-
resource: resource)
|
|
73
|
+
return nil unless valid_level?(level) && FieldLevel.row?(level, muting: muting?)
|
|
74
|
+
|
|
75
|
+
attrs = rows_of(resource).attrs(column, level)
|
|
76
|
+
return nil unless attrs
|
|
77
|
+
|
|
78
|
+
field = resource.resource_fields.build(attrs)
|
|
79
|
+
change(kind: :new_field, record: field, attribute: 'level', from: nil,
|
|
80
|
+
to: [FieldLevel.level_of(field, muting: muting?), field.mask_strategy], resource: resource)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def rows_of(resource)
|
|
84
|
+
@rows_of ||= {}
|
|
85
|
+
@rows_of[resource.id] ||= Services::ResourceFieldGenerator.rows(resource, muting: muting?, sensitive: sensitive)
|
|
59
86
|
end
|
|
60
87
|
end
|
|
61
88
|
end
|
|
@@ -10,9 +10,12 @@ module PermissionForge
|
|
|
10
10
|
TEXT = %w[label description].freeze
|
|
11
11
|
READ_DEPENDENT = %w[edit_access delete_access view_all modify_all].freeze
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
# @param refused [Array] collects a ChangeSet::Refusal for each flag
|
|
14
|
+
# the request turns on and the cascades keep off
|
|
15
|
+
def initialize(resource, muting:, refused: [])
|
|
14
16
|
@resource = resource
|
|
15
17
|
@muting = muting
|
|
18
|
+
@refused = refused
|
|
16
19
|
end
|
|
17
20
|
|
|
18
21
|
def changes(attrs)
|
|
@@ -29,6 +32,7 @@ module PermissionForge
|
|
|
29
32
|
# part of) the request.
|
|
30
33
|
def flag_changes(requested)
|
|
31
34
|
wanted = wanted_flags(requested)
|
|
35
|
+
note_refused(requested, wanted)
|
|
32
36
|
FLAGS.filter_map do |flag|
|
|
33
37
|
to = wanted[flag]
|
|
34
38
|
change(flag, to, kind: :resource_flag, cascaded: requested.fetch(flag, :absent) != to).assign
|
|
@@ -41,6 +45,18 @@ module PermissionForge
|
|
|
41
45
|
wanted
|
|
42
46
|
end
|
|
43
47
|
|
|
48
|
+
# A flag the request turns on that the cascades keep off (Edit without
|
|
49
|
+
# Read, say: a form the UI never sends) is not a change, so the review
|
|
50
|
+
# would say nothing about it. Noted, with what it needs.
|
|
51
|
+
def note_refused(requested, wanted)
|
|
52
|
+
requested.each do |flag, value|
|
|
53
|
+
next unless value && !wanted[flag] && !@resource.public_send(flag)
|
|
54
|
+
|
|
55
|
+
needs = flag == 'modify_all' && wanted['read_access'] ? :view_all_and_edit : :read
|
|
56
|
+
@refused << ChangeSet::Refusal.new(resource: @resource, attribute: flag, needs: needs)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
44
60
|
def cascade!(flags)
|
|
45
61
|
READ_DEPENDENT.each { |flag| flags[flag] = false } unless flags['read_access']
|
|
46
62
|
flags['modify_all'] = false unless flags['edit_access'] && flags['view_all']
|
|
@@ -10,7 +10,7 @@ module PermissionForge
|
|
|
10
10
|
resource = unlocked_resource(id)
|
|
11
11
|
next [] unless resource && attrs.is_a?(Hash)
|
|
12
12
|
|
|
13
|
-
ResourceEdit.new(resource, muting: muting
|
|
13
|
+
ResourceEdit.new(resource, muting: muting?, refused: refused).changes(attrs)
|
|
14
14
|
end
|
|
15
15
|
end
|
|
16
16
|
end
|
|
@@ -6,6 +6,7 @@ module PermissionForge
|
|
|
6
6
|
# Set header: name, description, active, group.
|
|
7
7
|
class SetAttrs < Base
|
|
8
8
|
SET_ATTRS = %w[name description is_active permission_set_group_id].freeze
|
|
9
|
+
TEXT_ATTRS = %w[name description].freeze
|
|
9
10
|
# A group id that is neither blank (no group) nor digits.
|
|
10
11
|
NOT_A_GROUP = Object.new.freeze
|
|
11
12
|
TEXT_CAST = ->(value) { value.to_s.strip.presence if value.is_a?(String) || value.is_a?(Numeric) }
|
|
@@ -36,7 +37,10 @@ module PermissionForge
|
|
|
36
37
|
EditableLock.locked_group_for_move(from: permission_set.permission_set_group_id, to: group_id).present?
|
|
37
38
|
end
|
|
38
39
|
|
|
40
|
+
# A blank name over a generated one, or a blank description over
|
|
41
|
+
# none, is no change (Prefill.posted).
|
|
39
42
|
def cast(attr, value)
|
|
43
|
+
value = Prefill.posted(permission_set, attr, value) if TEXT_ATTRS.include?(attr)
|
|
40
44
|
CASTS.fetch(attr, TEXT_CAST).call(value)
|
|
41
45
|
end
|
|
42
46
|
end
|
|
@@ -5,14 +5,20 @@ module PermissionForge
|
|
|
5
5
|
module ChangeSetParts
|
|
6
6
|
# Stateless value rules shared by the ChangeSet collaborators.
|
|
7
7
|
module Values
|
|
8
|
+
# A flag of the change set's params (`pf[...]`), read as every
|
|
9
|
+
# admin and API flag is (ParamShapes.boolean: the forms post "0" and
|
|
10
|
+
# "1"); nil is false.
|
|
11
|
+
#
|
|
12
|
+
# @raise [ParamShapes::InvalidFlag] for anything else: ActiveModel's
|
|
13
|
+
# cast read "nope" as true (the admin answers with a flash)
|
|
8
14
|
def self.truthy?(value)
|
|
9
|
-
|
|
15
|
+
value.nil? ? false : ParamShapes.flag('pf', value)
|
|
10
16
|
end
|
|
11
17
|
|
|
12
|
-
# Edit on a resource without Edit is downgraded to Read
|
|
13
|
-
#
|
|
18
|
+
# Edit on a resource without Edit is downgraded to Read
|
|
19
|
+
# (FieldLevel.capped, the one definition).
|
|
14
20
|
def self.effective_level(level, resource, muting:)
|
|
15
|
-
level
|
|
21
|
+
FieldLevel.capped(level, resource_edit: resource.edit_access, muting: muting)
|
|
16
22
|
end
|
|
17
23
|
end
|
|
18
24
|
end
|
|
@@ -2,18 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
module PermissionForge
|
|
4
4
|
module Admin
|
|
5
|
-
# Creates ResourceFields for model columns and virtual fields at one
|
|
6
|
-
#
|
|
7
|
-
#
|
|
5
|
+
# Creates ResourceFields for model columns and virtual fields at one
|
|
6
|
+
# level: the admin's presets ("Add resource"), "Add all new" and "Sync
|
|
7
|
+
# fields". The rows themselves come from the one row builder
|
|
8
|
+
# (Services::ResourceFieldGenerator::RowBuilder): privacy defaults,
|
|
9
|
+
# timestamps, the recommended strategy, the label. This class only turns
|
|
10
|
+
# a preset into a level per column — the muting 'hide_sensitive' preset
|
|
11
|
+
# hides the sensitive names and leaves the others alone — and skips the
|
|
12
|
+
# levels that are no row (Unchanged, a level the set's kind lacks).
|
|
13
|
+
#
|
|
14
|
+
# @api private
|
|
8
15
|
class ColumnFields
|
|
16
|
+
# The muting preset that hides the sensitive names and leaves the others.
|
|
17
|
+
HIDE_SENSITIVE = 'hide_sensitive'
|
|
18
|
+
|
|
9
19
|
def initialize(resource, muting:, sensitive: SensitiveColumns.new, configuration: PermissionForge.configuration)
|
|
10
20
|
@resource = resource
|
|
11
21
|
@muting = muting
|
|
12
22
|
@sensitive = sensitive
|
|
13
|
-
@
|
|
23
|
+
@rows = Services::ResourceFieldGenerator.rows(resource, muting: muting, configuration: configuration, sensitive: sensitive)
|
|
14
24
|
end
|
|
15
25
|
|
|
16
|
-
# @return [Integer] number of fields created (columns whose level resolves to nil are skipped)
|
|
26
|
+
# @return [Integer] number of fields created (columns whose level resolves to nil, and blank names, are skipped)
|
|
17
27
|
def create(columns, level)
|
|
18
28
|
columns.filter_map { |column| create_field(column, level) }.size
|
|
19
29
|
end
|
|
@@ -21,27 +31,18 @@ module PermissionForge
|
|
|
21
31
|
private
|
|
22
32
|
|
|
23
33
|
def create_field(column, level)
|
|
24
|
-
|
|
25
|
-
column_level = @muting ? muting_level(level, match) : granting_level(level, match)
|
|
34
|
+
column_level = column_level(column, level)
|
|
26
35
|
return unless column_level
|
|
27
36
|
|
|
28
|
-
attrs =
|
|
29
|
-
@resource.resource_fields.create!(attrs
|
|
30
|
-
mask_strategy: match&.strategy || 'full'))
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def muting_level(level, match)
|
|
34
|
-
return 'hide' if level == 'hide_sensitive' && match
|
|
35
|
-
return nil if %w[hide_sensitive leave].include?(level) || level.blank?
|
|
36
|
-
|
|
37
|
-
FieldLevel.valid?(level, muting: true) ? level : nil
|
|
37
|
+
attrs = @rows.attrs(column, column_level)
|
|
38
|
+
@resource.resource_fields.create!(attrs) if attrs
|
|
38
39
|
end
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return
|
|
41
|
+
# @return [String, nil] the level the column's row gets, nil for no row
|
|
42
|
+
def column_level(column, level)
|
|
43
|
+
return level if FieldLevel.row?(level, muting: @muting)
|
|
43
44
|
|
|
44
|
-
level
|
|
45
|
+
'hide' if @muting && level == HIDE_SENSITIVE && @sensitive.sensitive?(column)
|
|
45
46
|
end
|
|
46
47
|
end
|
|
47
48
|
end
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'ipaddr'
|
|
4
|
-
|
|
5
3
|
module PermissionForge
|
|
6
4
|
module Admin
|
|
7
5
|
# Activation conditions form (Conditions tab): turns the posted IP list,
|
|
8
6
|
# hour window and session flag into PermissionSet attributes, collecting
|
|
9
|
-
# per-input errors (:ips => [messages], :
|
|
7
|
+
# per-input errors (:ips => [messages], :hours => message, :flag =>
|
|
8
|
+
# message). What it refuses is what ActivationConditions cannot judge,
|
|
9
|
+
# for the same reason: an entry ActivationConditions.ip_range does not
|
|
10
|
+
# read as an address, a window whose HoursWindow#problem is not nil.
|
|
10
11
|
class ConditionsForm
|
|
12
|
+
# The hours the tab shows before anything is chosen.
|
|
13
|
+
DEFAULT_HOURS = { from: 8, to: 18 }.freeze
|
|
11
14
|
FLAG_FORMAT = /\A[\w.:-]{1,100}\z/
|
|
12
15
|
|
|
13
16
|
attr_reader :errors
|
|
@@ -18,25 +21,41 @@ module PermissionForge
|
|
|
18
21
|
@errors = {}
|
|
19
22
|
end
|
|
20
23
|
|
|
21
|
-
# Evaluated in order, so errors are keyed :ips
|
|
24
|
+
# Evaluated in order, so errors are keyed :ips, then :hours, then :flag.
|
|
22
25
|
def attributes
|
|
23
26
|
@attributes ||= {
|
|
24
27
|
allowed_ips: ips.join(',').presence,
|
|
25
28
|
allowed_hours_start: hour(:allowed_hours_start),
|
|
26
29
|
allowed_hours_end: hour(:allowed_hours_end),
|
|
27
30
|
requires_session_flag: session_flag
|
|
28
|
-
}
|
|
31
|
+
}.tap { check_hours }
|
|
29
32
|
end
|
|
30
33
|
|
|
31
34
|
private
|
|
32
35
|
|
|
36
|
+
# The entries as the engine reads them (ActivationConditions.entries:
|
|
37
|
+
# commas, newlines or spaces between them); an entry
|
|
38
|
+
# ActivationConditions.ip_range does not read as an address is refused.
|
|
33
39
|
def ips
|
|
34
|
-
list = @params[:allowed_ips]
|
|
35
|
-
bad = list.each_with_index.reject { |ip, _|
|
|
40
|
+
list = PermissionForge::ActivationConditions.entries(@params[:allowed_ips])
|
|
41
|
+
bad = list.each_with_index.reject { |ip, _| PermissionForge::ActivationConditions.ip_range(ip) }
|
|
36
42
|
@errors[:ips] = bad.map { |ip, i| I18n.t('permission_forge.errors.admin.ip', line: i + 1, ip: ip) } if bad.any?
|
|
37
43
|
list
|
|
38
44
|
end
|
|
39
45
|
|
|
46
|
+
# A window nobody can judge (HoursWindow#problem: one hour of the two,
|
|
47
|
+
# an hour outside 0–23, the same hour twice) is refused by the model
|
|
48
|
+
# (PermissionSet#hours_window_is_judgeable); said here too, keyed
|
|
49
|
+
# :hours, so the form shows it next to the selects.
|
|
50
|
+
def check_hours
|
|
51
|
+
problem = posted_window.problem
|
|
52
|
+
@errors[:hours] = I18n.t("permission_forge.errors.admin.#{problem}") if problem
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def posted_window
|
|
56
|
+
PermissionForge::ActivationConditions::HoursWindow.new(from: hour(:allowed_hours_start), to: hour(:allowed_hours_end))
|
|
57
|
+
end
|
|
58
|
+
|
|
40
59
|
public
|
|
41
60
|
|
|
42
61
|
# Whether the hour window was switched on in the posted form.
|
|
@@ -50,6 +69,27 @@ module PermissionForge
|
|
|
50
69
|
RecordId.cast(@params[key])
|
|
51
70
|
end
|
|
52
71
|
|
|
72
|
+
# The hour window the Conditions tab shows: the posted hours of a
|
|
73
|
+
# refused form (`form`), else the set's (HoursWindow; the tab asks it
|
|
74
|
+
# whether it is set, wraps midnight or is broken, never compares the
|
|
75
|
+
# hours itself).
|
|
76
|
+
#
|
|
77
|
+
# @param set [PermissionSet]
|
|
78
|
+
# @param form [ConditionsForm, nil]
|
|
79
|
+
# @return [ActivationConditions::HoursWindow]
|
|
80
|
+
def self.window(set, form)
|
|
81
|
+
stored = set.hours_window
|
|
82
|
+
return stored unless form
|
|
83
|
+
|
|
84
|
+
PermissionForge::ActivationConditions::HoursWindow.new(from: form.submitted_hour(:allowed_hours_start) || stored.from,
|
|
85
|
+
to: form.submitted_hour(:allowed_hours_end) || stored.to)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Whether the tab shows the hour window switched on: as posted, else as stored.
|
|
89
|
+
def self.hours_on?(set, form)
|
|
90
|
+
form ? form.hours_enabled? : set.hours_window.set?
|
|
91
|
+
end
|
|
92
|
+
|
|
53
93
|
private
|
|
54
94
|
|
|
55
95
|
# nil unless the hour window is enabled.
|
|
@@ -65,14 +105,6 @@ module PermissionForge
|
|
|
65
105
|
@errors[:flag] = I18n.t('permission_forge.errors.admin.flag')
|
|
66
106
|
nil
|
|
67
107
|
end
|
|
68
|
-
|
|
69
|
-
# IPAddr::Error is an ArgumentError.
|
|
70
|
-
def valid_ip?(value)
|
|
71
|
-
IPAddr.new(value)
|
|
72
|
-
true
|
|
73
|
-
rescue ArgumentError
|
|
74
|
-
false
|
|
75
|
-
end
|
|
76
108
|
end
|
|
77
109
|
end
|
|
78
110
|
end
|
|
@@ -8,10 +8,11 @@ module PermissionForge
|
|
|
8
8
|
#
|
|
9
9
|
# Reads the route table once per instance. Routes without a static
|
|
10
10
|
# controller and action (mounted engines and Rack apps, redirects, dynamic
|
|
11
|
-
# segments), internal routes
|
|
12
|
-
# cannot be a ResourceAction key are
|
|
13
|
-
#
|
|
14
|
-
# that
|
|
11
|
+
# segments), internal routes and excluded controllers are skipped; actions
|
|
12
|
+
# whose name cannot be a ResourceAction key are left out of #entries (no
|
|
13
|
+
# row can manage them) but kept in #mapped_entries, since the guard still
|
|
14
|
+
# checks them. A controller that fails to load, or a host resolver that
|
|
15
|
+
# raises, drops that one entry instead of the whole catalog.
|
|
15
16
|
#
|
|
16
17
|
# @example
|
|
17
18
|
# catalog = ControllerCatalog.new
|
|
@@ -26,11 +27,16 @@ module PermissionForge
|
|
|
26
27
|
# One mapped controller action. `key` is the action key the guard asks
|
|
27
28
|
# for; `crud` is :read / :create / :edit / :delete when the action means
|
|
28
29
|
# a CRUD permission, nil for a custom action. `guarded` is whether the
|
|
29
|
-
# controller runs the guard (ControllerActions.guarded?).
|
|
30
|
+
# controller runs the guard (ControllerActions.guarded?). `manageable`
|
|
31
|
+
# is whether a row can manage it (Mapping::Target#manageable?).
|
|
30
32
|
Entry = Struct.new(:controller_path, :action_name, :verbs, :paths, :resource, :key, :crud, :guarded,
|
|
31
|
-
keyword_init: true) do
|
|
33
|
+
:manageable, keyword_init: true) do
|
|
32
34
|
def crud? = !crud.nil?
|
|
33
35
|
def guarded? = guarded == true
|
|
36
|
+
def manageable? = manageable == true
|
|
37
|
+
|
|
38
|
+
# ['Customer', 'export']: what a ResourceAction row of that resource and name manages
|
|
39
|
+
def pair = [resource, key]
|
|
34
40
|
|
|
35
41
|
# 'customers#export'
|
|
36
42
|
def endpoint = "#{controller_path}##{action_name}"
|
|
@@ -82,13 +88,23 @@ module PermissionForge
|
|
|
82
88
|
# controller_path => conventional resource, filled by Mapping: one
|
|
83
89
|
# lookup per controller instead of one per route.
|
|
84
90
|
@conventions = {}
|
|
91
|
+
@skipped = []
|
|
85
92
|
end
|
|
86
93
|
|
|
87
|
-
# @return [Array<Entry>] one per mapped (controller_path, action_name)
|
|
88
|
-
# sorted by resource, key, controller and action
|
|
94
|
+
# @return [Array<Entry>] one per mapped (controller_path, action_name)
|
|
95
|
+
# a row can manage, sorted by resource, key, controller and action
|
|
89
96
|
def entries
|
|
90
|
-
@entries ||=
|
|
91
|
-
|
|
97
|
+
@entries ||= mapped_entries.select(&:manageable?).freeze
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Every mapped controller action, those whose name cannot be an action
|
|
101
|
+
# key included: the guard checks them too (with no row possible, the
|
|
102
|
+
# strategy for unmanaged actions decides).
|
|
103
|
+
#
|
|
104
|
+
# @return [Array<Entry>] sorted as #entries
|
|
105
|
+
def mapped_entries
|
|
106
|
+
@mapped_entries ||= routed_actions.filter_map { |(path, action), route| build_entry(path, action, route) }
|
|
107
|
+
.sort_by(&:sort_key).freeze
|
|
92
108
|
end
|
|
93
109
|
|
|
94
110
|
# @param resource_name [String, Symbol] e.g. 'Customer'
|
|
@@ -104,6 +120,17 @@ module PermissionForge
|
|
|
104
120
|
for_resource(resource_name).map(&:key).uniq
|
|
105
121
|
end
|
|
106
122
|
|
|
123
|
+
# What reading the routes could not judge: a controller that fails to
|
|
124
|
+
# load (listed, but whether it runs the guard is unknown) and a
|
|
125
|
+
# controller action dropped because its controller, the resolver or the
|
|
126
|
+
# mapping raised. The log has the first error.
|
|
127
|
+
#
|
|
128
|
+
# @return [Array<String>] "customers#export: RuntimeError", "broken: NameError"
|
|
129
|
+
def skipped
|
|
130
|
+
mapped_entries
|
|
131
|
+
@skipped.dup.freeze
|
|
132
|
+
end
|
|
133
|
+
|
|
107
134
|
# @return [Boolean] whether any listed controller runs the guard (see
|
|
108
135
|
# ControllerActions.guarded?). When false, permitting or blocking an
|
|
109
136
|
# action changes nothing at runtime.
|
|
@@ -130,11 +157,11 @@ module PermissionForge
|
|
|
130
157
|
klass = controller_class(path)
|
|
131
158
|
target = ControllerActions::Mapping.call(controller_path: path, action_name: action, controller_class: klass,
|
|
132
159
|
configuration: @configuration, conventions: @conventions)
|
|
133
|
-
return nil unless target
|
|
160
|
+
return nil unless target
|
|
134
161
|
|
|
135
162
|
Entry.new(controller_path: path, action_name: action, verbs: route[:verbs].sort, paths: route[:paths].sort,
|
|
136
163
|
resource: target.resource, key: target.action, crud: target.crud,
|
|
137
|
-
guarded: ControllerActions.guarded?(klass))
|
|
164
|
+
guarded: ControllerActions.guarded?(klass), manageable: target.manageable?)
|
|
138
165
|
rescue StandardError => e
|
|
139
166
|
log_skipped("#{path}##{action}", e)
|
|
140
167
|
nil
|
|
@@ -153,9 +180,10 @@ module PermissionForge
|
|
|
153
180
|
nil
|
|
154
181
|
end
|
|
155
182
|
|
|
156
|
-
#
|
|
157
|
-
# same line for every route.
|
|
183
|
+
# Kept for #skipped; logged once per catalog: a raising resolver would
|
|
184
|
+
# otherwise repeat the same line for every route.
|
|
158
185
|
def log_skipped(what, error)
|
|
186
|
+
@skipped << "#{what}: #{error.class}"
|
|
159
187
|
return if @logged
|
|
160
188
|
|
|
161
189
|
@logged = true
|
|
@@ -23,10 +23,20 @@ module PermissionForge
|
|
|
23
23
|
# those changes goes the other way: adding a holder or keeping them
|
|
24
24
|
# longer narrows, removing one or ending their assignment sooner widens.
|
|
25
25
|
#
|
|
26
|
+
# "Locked" has one definition, here, for the controllers, the API, the
|
|
27
|
+
# read models and the views alike: .locked?(record) walks the chain, and
|
|
28
|
+
# .permits?(record, action) says whether the admin may offer an action
|
|
29
|
+
# on a record — the one table the controllers' refusals and the views'
|
|
30
|
+
# "show, disable or leave out" both read (html/locked_pages_test.rb
|
|
31
|
+
# checks every control of every page rendered for a locked record
|
|
32
|
+
# against it).
|
|
33
|
+
#
|
|
26
34
|
# @example
|
|
27
35
|
# EditableLock.locked_record(field) # => the locked Resource or Permission Set, or nil
|
|
36
|
+
# EditableLock.locked?(field) # => true when the field, its Resource or its set is locked
|
|
28
37
|
# EditableLock.chain(rule) # => [rule, resource, permission_set]
|
|
29
38
|
# EditableLock.widening_lock(set, :revoke) # => the set when it is locked and muting, else nil
|
|
39
|
+
# EditableLock.permits?(assignment, :revoke) # => false on a locked Muting Permission Set
|
|
30
40
|
module EditableLock
|
|
31
41
|
# What each record belongs to, for the lock: the first that is present.
|
|
32
42
|
PARENTS = {
|
|
@@ -42,6 +52,13 @@ module PermissionForge
|
|
|
42
52
|
# locked container refuses the deletion (a set in a locked group).
|
|
43
53
|
CONTAINERS = { 'PermissionForge::PermissionSet' => :permission_set_group }.freeze
|
|
44
54
|
|
|
55
|
+
# The changes to who holds a set (.widens_access?): judged by direction,
|
|
56
|
+
# on the set the record is or belongs to.
|
|
57
|
+
HOLDER_CHANGES = %i[grant revoke change_expiry].freeze
|
|
58
|
+
# Actions a lock never refuses: a copy is editable, a review changes no
|
|
59
|
+
# access, offboarding and a cache refresh narrow or change nothing.
|
|
60
|
+
ALWAYS_PERMITTED = %i[duplicate review revoke_all refresh consent erasure].freeze
|
|
61
|
+
|
|
45
62
|
module_function
|
|
46
63
|
|
|
47
64
|
# @param record [ActiveRecord::Base, nil]
|
|
@@ -59,17 +76,47 @@ module PermissionForge
|
|
|
59
76
|
# @return [ActiveRecord::Base, nil] the first locked record in their chains
|
|
60
77
|
def locked_record(*records)
|
|
61
78
|
records.flatten.compact.each do |record|
|
|
62
|
-
locked = chain(record).find { |link|
|
|
79
|
+
locked = chain(record).find { |link| flagged?(link) }
|
|
63
80
|
return locked if locked
|
|
64
81
|
end
|
|
65
82
|
nil
|
|
66
83
|
end
|
|
67
84
|
|
|
68
|
-
#
|
|
85
|
+
# Whether the record is locked: itself, or what it belongs to (its
|
|
86
|
+
# chain) flagged is_editable: false. The one answer for a view's
|
|
87
|
+
# "locked" and a controller's refusal.
|
|
88
|
+
#
|
|
89
|
+
# @param record [ActiveRecord::Base, nil]
|
|
90
|
+
# @return [Boolean]
|
|
69
91
|
def locked?(record)
|
|
92
|
+
!locked_record(record).nil?
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# @return [Boolean] whether the record itself carries the flag (is_editable: false)
|
|
96
|
+
def flagged?(record)
|
|
70
97
|
record.respond_to?(:is_editable) && record.is_editable == false
|
|
71
98
|
end
|
|
72
99
|
|
|
100
|
+
# Whether the admin may offer `action` on `record`: a change to the
|
|
101
|
+
# holders of a set (:grant, :revoke, :change_expiry with from:/to:)
|
|
102
|
+
# goes by direction (.widening_lock); ALWAYS_PERMITTED actions always
|
|
103
|
+
# may; :delete also asks the record's container (.container_of); any
|
|
104
|
+
# other action (:edit, :add, :toggle, :sync, :acknowledge, ...) changes
|
|
105
|
+
# what the record holds and is refused while the record is locked.
|
|
106
|
+
#
|
|
107
|
+
# @param record [ActiveRecord::Base] the record the action is on (an
|
|
108
|
+
# assignment or its set for a holder change)
|
|
109
|
+
# @param action [Symbol]
|
|
110
|
+
# @param expiry [Hash] from:, to: for :change_expiry
|
|
111
|
+
# @return [Boolean]
|
|
112
|
+
def permits?(record, action, **expiry)
|
|
113
|
+
return true if ALWAYS_PERMITTED.include?(action)
|
|
114
|
+
return holder_change_permitted?(record, action, **expiry) if HOLDER_CHANGES.include?(action)
|
|
115
|
+
return false if locked?(record)
|
|
116
|
+
|
|
117
|
+
action != :delete || !flagged?(container_of(record))
|
|
118
|
+
end
|
|
119
|
+
|
|
73
120
|
# Moving a set from one Permission Set Group to another changes both
|
|
74
121
|
# groups' lists of sets: refused when either is locked (the group
|
|
75
122
|
# page's add/remove set refuses the same).
|
|
@@ -81,7 +128,7 @@ module PermissionForge
|
|
|
81
128
|
to = RecordId.cast(to)
|
|
82
129
|
return nil if to == from
|
|
83
130
|
|
|
84
|
-
PermissionForge::PermissionSetGroup.where(id: [from, to].compact).detect { |group|
|
|
131
|
+
PermissionForge::PermissionSetGroup.where(id: [from, to].compact).detect { |group| flagged?(group) }
|
|
85
132
|
end
|
|
86
133
|
|
|
87
134
|
# The container whose list deleting `record` would change, or nil.
|
|
@@ -92,6 +139,21 @@ module PermissionForge
|
|
|
92
139
|
association && record.public_send(association)
|
|
93
140
|
end
|
|
94
141
|
|
|
142
|
+
# The set a holder change is on: the record when it is one, else the
|
|
143
|
+
# set an assignment belongs to; nil for a group or its membership.
|
|
144
|
+
def permission_set_of(record)
|
|
145
|
+
set = record.respond_to?(:permission_set) ? record.permission_set : record
|
|
146
|
+
set if set.is_a?(PermissionForge::PermissionSet)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# A holder change on a set goes by direction (.widening_lock); one on a
|
|
150
|
+
# Permission Set Group (a membership, or the group) has none — a
|
|
151
|
+
# locked group freezes its members both ways.
|
|
152
|
+
def holder_change_permitted?(record, action, **expiry)
|
|
153
|
+
set = permission_set_of(record)
|
|
154
|
+
set ? widening_lock(set, action, **expiry).nil? : !locked?(record)
|
|
155
|
+
end
|
|
156
|
+
|
|
95
157
|
# How a change moves a person's holding of a set: +1 they hold it more
|
|
96
158
|
# (added, or kept longer), -1 less (removed, or ended sooner), 0 not at
|
|
97
159
|
# all (the same end date again).
|
|
@@ -127,6 +189,24 @@ module PermissionForge
|
|
|
127
189
|
locked_record(permission_set)
|
|
128
190
|
end
|
|
129
191
|
|
|
192
|
+
# The end-date changes a "Change end date" menu (or Home's "Extend 30
|
|
193
|
+
# days", ExpiryPresets::EXTEND_KEY) offers for `assignment`: those the
|
|
194
|
+
# lock lets through (.widening_lock), so a locked set never offers a
|
|
195
|
+
# choice that would be refused.
|
|
196
|
+
#
|
|
197
|
+
# @param assignment [PermissionForge::PermissionSetUser]
|
|
198
|
+
# @param keys [Array<String>] the changes to choose from (ExpiryPresets.change_to keys)
|
|
199
|
+
# @return [Array<String>]
|
|
200
|
+
def expiry_choices(assignment, keys: ExpiryPresets::CHANGE_KEYS, today: Time.zone.today)
|
|
201
|
+
set = assignment.permission_set
|
|
202
|
+
return keys unless locked_record(set)
|
|
203
|
+
|
|
204
|
+
keys.reject do |key|
|
|
205
|
+
to = ExpiryPresets.change_to(key, from: assignment.expires_at, today: today)
|
|
206
|
+
widening_lock(set, :change_expiry, from: assignment.expires_at, to: to)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
130
210
|
# +1 when the end date moves later or is cleared, -1 when it moves
|
|
131
211
|
# earlier or is set where there was none, 0 when it stays. Compared at
|
|
132
212
|
# the column's precision (microseconds): a preset computes its end with
|