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
|
@@ -26,6 +26,22 @@ module PermissionForge
|
|
|
26
26
|
next unless ended
|
|
27
27
|
|
|
28
28
|
t('permission_forge.audit_log.sentence_variants.assignment_revoked_expired', **parts, date: pf_date(ended))
|
|
29
|
+
},
|
|
30
|
+
# A clear the cache store did not confirm is not told as cleared.
|
|
31
|
+
'cache.cleared' => lambda { |event, parts|
|
|
32
|
+
next unless pf_audit_cache_unconfirmed?(event)
|
|
33
|
+
|
|
34
|
+
t('permission_forge.audit_log.sentence_variants.cache_cleared_unconfirmed', **parts)
|
|
35
|
+
},
|
|
36
|
+
'permissions.invalidated' => lambda { |event, parts|
|
|
37
|
+
next unless pf_audit_cache_unconfirmed?(event)
|
|
38
|
+
|
|
39
|
+
t('permission_forge.audit_log.sentence_variants.permissions_invalidated_unconfirmed', **parts)
|
|
40
|
+
},
|
|
41
|
+
'cache.settled' => lambda { |event, parts|
|
|
42
|
+
next unless pf_audit_cache_unconfirmed?(event)
|
|
43
|
+
|
|
44
|
+
t('permission_forge.audit_log.sentence_variants.cache_settled_unconfirmed', **parts)
|
|
29
45
|
}
|
|
30
46
|
}.freeze
|
|
31
47
|
|
|
@@ -33,13 +49,51 @@ module PermissionForge
|
|
|
33
49
|
# (permission_forge.audit_log.sentences.<event>): a new event needs one
|
|
34
50
|
# key in each locale, and an event without one (a host's own) still reads
|
|
35
51
|
# as "<actor>: <event name>". Interpolations: actor, record, set, person,
|
|
36
|
-
# group, event, model (a runtime event's model).
|
|
52
|
+
# group, event, model (a runtime event's model). An event whose cache
|
|
53
|
+
# work reached a per-process store (its Cache::Outcome:
|
|
54
|
+
# data['process_local']) ends with the one cache-scope suffix, whatever
|
|
55
|
+
# the event: which processes kept their entries depends on where it ran
|
|
56
|
+
# (data['in_request']).
|
|
37
57
|
def pf_audit_sentence(event, lookups)
|
|
38
58
|
parts = pf_audit_sentence_parts(event, lookups)
|
|
39
59
|
variant = AUDIT_SENTENCE_VARIANTS[event.event]
|
|
40
60
|
variant &&= instance_exec(event, parts, &variant)
|
|
41
|
-
|
|
61
|
+
sentence = variant || pf_audit_plain_sentence(event, parts)
|
|
62
|
+
suffix = pf_audit_cache_scope_suffix(event)
|
|
63
|
+
suffix ? "#{sentence} #{suffix}" : sentence
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Whether the event's cache work reached a per-process in-memory store
|
|
67
|
+
# (data['process_local'], Cache::Base#process_local?).
|
|
68
|
+
def pf_audit_process_local?(event)
|
|
69
|
+
event.data.is_a?(Hash) && event.data['process_local'] == true
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Whether the store said no to the event's cache work: its Cache::Outcome
|
|
73
|
+
# (`cache_confirmed`), or a permissions.invalidated event's `confirmed`.
|
|
74
|
+
def pf_audit_cache_unconfirmed?(event)
|
|
75
|
+
data = event.data
|
|
76
|
+
data.is_a?(Hash) && (data['cache_confirmed'] == false || data['confirmed'] == false)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# The suffix for a per-process store, from where the work ran
|
|
80
|
+
# (Cache::Outcome#kept_by) and what the store said together: cleared in
|
|
81
|
+
# a server process (the other web workers and the job processes kept
|
|
82
|
+
# theirs), cleared in the process that ran it (every other process kept
|
|
83
|
+
# theirs — also what an event recorded without `in_request` reads as:
|
|
84
|
+
# it never names the web processes), or not confirmed (nothing is said
|
|
85
|
+
# to be cleared anywhere).
|
|
86
|
+
def pf_audit_cache_scope_suffix(event)
|
|
87
|
+
return unless pf_audit_process_local?(event)
|
|
88
|
+
|
|
89
|
+
scope = if pf_audit_cache_unconfirmed?(event) then 'unconfirmed'
|
|
90
|
+
elsif event.data['in_request'] == true then 'server_process'
|
|
91
|
+
else 'own_process'
|
|
92
|
+
end
|
|
93
|
+
t("permission_forge.audit_log.cache_scope.#{scope}")
|
|
94
|
+
end
|
|
42
95
|
|
|
96
|
+
def pf_audit_plain_sentence(event, parts)
|
|
43
97
|
fallback = t("#{AUDIT_SENTENCES}.fallback", **parts)
|
|
44
98
|
sentence = t("#{AUDIT_SENTENCES}.#{event.event}", **parts, default: fallback)
|
|
45
99
|
sentence.is_a?(String) ? sentence : fallback
|
|
@@ -4,8 +4,13 @@ module PermissionForge
|
|
|
4
4
|
#
|
|
5
5
|
# @api private
|
|
6
6
|
module BadgesHelper
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
# The level-badge colour of a state that is not a field level
|
|
8
|
+
# (Admin::StateLabel kinds :effective and :action), by its kind.
|
|
9
|
+
LEVEL_BADGE_CSS = {
|
|
10
|
+
effective: { 'hidden_by_muting' => 'muted' },
|
|
11
|
+
action: { 'permitted' => 'edit', 'not_permitted' => 'none', 'blocked' => 'muted', 'unchanged' => 'leave',
|
|
12
|
+
'none' => 'none' }
|
|
13
|
+
}.freeze
|
|
9
14
|
|
|
10
15
|
MASK_SAMPLES = [
|
|
11
16
|
[/email/, 'james.walker@acme.com'],
|
|
@@ -35,8 +40,19 @@ module PermissionForge
|
|
|
35
40
|
content_tag(:span, content, class: classes, title: opts[:title])
|
|
36
41
|
end
|
|
37
42
|
|
|
38
|
-
|
|
39
|
-
|
|
43
|
+
# The Locked badge. With a record: only when it is locked (pf_locked?,
|
|
44
|
+
# the chain), so a row's badge and its disabled controls agree.
|
|
45
|
+
def pf_lock_badge(record = nil, title: pf_ui('locked.title'))
|
|
46
|
+
return if !record.nil? && !pf_locked?(record)
|
|
47
|
+
|
|
48
|
+
pf_badge([pf_lock_icon, pf_state(:lock, :locked).word], title: title)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Whether a record is locked: itself, its Resource, its Permission Set or
|
|
52
|
+
# (a membership) its group flagged is_editable: false — Admin::EditableLock,
|
|
53
|
+
# the one definition the controllers refuse by.
|
|
54
|
+
def pf_locked?(record)
|
|
55
|
+
PermissionForge::Admin::EditableLock.locked?(record)
|
|
40
56
|
end
|
|
41
57
|
|
|
42
58
|
def pf_off_badge
|
|
@@ -61,14 +77,10 @@ module PermissionForge
|
|
|
61
77
|
title: pf_condition_sentence(permission_set))
|
|
62
78
|
badges << pf_fail_open_badge if pf_fail_open?
|
|
63
79
|
end
|
|
64
|
-
badges << pf_lock_badge
|
|
80
|
+
badges << pf_lock_badge(permission_set)
|
|
65
81
|
safe_join(badges, ' ')
|
|
66
82
|
end
|
|
67
83
|
|
|
68
|
-
def pf_locked_record?(record)
|
|
69
|
-
record.respond_to?(:is_editable) && record.is_editable == false
|
|
70
|
-
end
|
|
71
|
-
|
|
72
84
|
# Permission Set chip. state: :normal / :inactive / :expired (auto from set).
|
|
73
85
|
def pf_set_chip(permission_set, large: false, href: :auto, expired: false)
|
|
74
86
|
state = pf_set_chip_state(permission_set, expired)
|
|
@@ -87,23 +99,68 @@ module PermissionForge
|
|
|
87
99
|
|
|
88
100
|
def pf_set_chip_body(permission_set, state)
|
|
89
101
|
suffix_key = CHIP_SUFFIX_KEYS[state]
|
|
90
|
-
safe_join([content_tag(:span, "#{CHIP_PREFIXES[state]}#{permission_set
|
|
102
|
+
safe_join([content_tag(:span, "#{CHIP_PREFIXES[state]}#{pf_record_name(permission_set)}"),
|
|
91
103
|
(content_tag(:span, pf_ui(suffix_key), class: 'pf-chip__suffix') if suffix_key)].compact, ' ')
|
|
92
104
|
end
|
|
93
105
|
|
|
94
|
-
#
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
106
|
+
# A field's level for one person (an EffectiveAccess::FieldOutcome), the
|
|
107
|
+
# same in their Effective access row, its Why panel and the field
|
|
108
|
+
# drawer's "Explain for": hidden by a Muting Permission Set reads
|
|
109
|
+
# "⊘ Hidden by muting", never the neutral "∅ Hidden" of a field no set
|
|
110
|
+
# grants. `sample: true` adds a masked value's preview (the Why panel).
|
|
111
|
+
def pf_effective_field_badge(field, sample: false)
|
|
112
|
+
return pf_level_badge('hidden_by_muting', kind: :effective) if field.hidden_by_muting?
|
|
113
|
+
|
|
114
|
+
masked = field.level == 'masked'
|
|
115
|
+
pf_level_badge(field.level, strategy: (field.mask_strategy if masked), sample_for: (field.name if masked && sample))
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# [glyph, sentence] for the entry along a single-table-inheritance chain
|
|
119
|
+
# that decides a person's field (EffectiveAccess::FieldOutcome#decided_by,
|
|
120
|
+
# #unreadable, #reason), or nil when the resource's own entry decides and
|
|
121
|
+
# grants Read: the Why panel and the field drawer's "Explain for" both
|
|
122
|
+
# say it, and the Effective access row says it when it is the reason
|
|
123
|
+
# (FieldOutcome#reason :unreadable or :inherited, B#39).
|
|
124
|
+
def pf_field_chain_reason(field)
|
|
125
|
+
base = field.decided_by
|
|
126
|
+
if field.unreadable
|
|
127
|
+
return ['∅', t('permission_forge.people.effective_why.field_own_unreadable')] unless base
|
|
128
|
+
|
|
129
|
+
return ['∅', t('permission_forge.people.effective_why.field_base_unreadable', resource: pf_resource_label(base))]
|
|
130
|
+
end
|
|
131
|
+
return unless base
|
|
132
|
+
return ['↑', t('permission_forge.people.effective_why.field_hidden_on', resource: pf_resource_label(base))] if field.reason == :inherited
|
|
133
|
+
|
|
134
|
+
['↑', t('permission_forge.people.effective_why.field_decided_by', resource: pf_resource_label(base))]
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# A level as a badge ("•• Masked (Email) · j***@acme.com"): a field level
|
|
138
|
+
# by default, or a state of another kind (Admin::StateLabel) drawn the
|
|
139
|
+
# same way — an effective outcome, an action state.
|
|
140
|
+
def pf_level_badge(level, strategy: nil, sample_for: nil, kind: :field_level)
|
|
141
|
+
label = PermissionForge::Admin::StateLabel.for(kind, level)
|
|
142
|
+
text = label.word
|
|
143
|
+
if %w[masked mask].include?(label.state) && strategy.present?
|
|
99
144
|
text = "#{text} (#{pf_mask_strategy_label(strategy)})"
|
|
100
145
|
text = "#{text} · #{pf_mask_preview(sample_for, strategy)}" if sample_for
|
|
101
146
|
end
|
|
102
|
-
|
|
147
|
+
css = LEVEL_BADGE_CSS.fetch(kind.to_sym, {}).fetch(label.state, label.state)
|
|
148
|
+
content_tag(:span, "#{label.glyph} #{text}", class: "pf-level-badge pf-level-badge--#{css}")
|
|
103
149
|
end
|
|
104
150
|
|
|
151
|
+
# The word of a field level (Admin::StateLabel, kind :field_level).
|
|
105
152
|
def pf_level_label(level)
|
|
106
|
-
|
|
153
|
+
pf_state(:field_level, level).word
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# The caption of a Fields tab filter: a level's word, Sensitive, or "All".
|
|
157
|
+
def pf_field_filter_label(filter)
|
|
158
|
+
case filter.to_s
|
|
159
|
+
when 'all' then t('permission_forge.resources.fields_tab.filters.all')
|
|
160
|
+
when 'sensitive' then pf_ui('badges.sensitive')
|
|
161
|
+
when 'unconfigured' then pf_state(:field_level, :none).word
|
|
162
|
+
else pf_level_label(filter)
|
|
163
|
+
end
|
|
107
164
|
end
|
|
108
165
|
|
|
109
166
|
def pf_mask_strategy_label(strategy)
|
|
@@ -42,18 +42,27 @@ module PermissionForge
|
|
|
42
42
|
"#{text} #{pf_action_managed_note(resource, action.name)}"
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
-
# The first row for an action makes it managed for everyone
|
|
46
|
-
#
|
|
45
|
+
# The first row for an action makes it managed for everyone (under :allow
|
|
46
|
+
# that restricts it; under :block, the default, it opens it to the people
|
|
47
|
+
# permitted); later rows join the sets that already manage it.
|
|
47
48
|
def pf_action_managed_note(resource, key)
|
|
48
49
|
return pf_ui('changes.action_managed_later') unless resource.in_effect?
|
|
49
50
|
|
|
50
51
|
across = pf_action_across(resource.name)
|
|
51
|
-
return
|
|
52
|
+
return pf_action_managed_first unless across.managed?(key)
|
|
52
53
|
|
|
53
54
|
sets = across.permitting_sets(key).map(&:name)
|
|
54
55
|
sets.any? ? pf_ui('changes.action_managed_already', sets: sets.join(', ')) : pf_ui('changes.action_managed_nobody')
|
|
55
56
|
end
|
|
56
57
|
|
|
58
|
+
def pf_action_managed_first
|
|
59
|
+
if PermissionForge.configuration.allow_unconfigured_actions?
|
|
60
|
+
pf_ui('changes.action_managed_first_allowed')
|
|
61
|
+
else
|
|
62
|
+
pf_ui('changes.action_managed_first_blocked')
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
57
66
|
# One lookup per resource name per page.
|
|
58
67
|
def pf_action_across(resource_name)
|
|
59
68
|
(@pf_action_across ||= {})[resource_name] ||= PermissionForge::Admin::ActionAcrossSets.new(resource_name)
|
|
@@ -64,7 +73,7 @@ module PermissionForge
|
|
|
64
73
|
when 'is_active' then pf_ui(change.to ? 'changes.set_on' : 'changes.set_off')
|
|
65
74
|
when 'permission_set_group_id'
|
|
66
75
|
group = change.to && PermissionForge::PermissionSetGroup.find_by(id: change.to)
|
|
67
|
-
group ? pf_ui('changes.set_group', group: group
|
|
76
|
+
group ? pf_ui('changes.set_group', group: pf_record_name(group)) : pf_ui('changes.set_no_group')
|
|
68
77
|
else pf_ui('changes.set_text', attribute: pf_ui("changes.attributes.#{change.attribute}"))
|
|
69
78
|
end
|
|
70
79
|
end
|
|
@@ -76,6 +85,13 @@ module PermissionForge
|
|
|
76
85
|
change.cascaded ? "#{text} #{pf_ui('changes.cascaded')}" : text
|
|
77
86
|
end
|
|
78
87
|
|
|
88
|
+
# A flag the request turned on that stays off (ChangeSet#refused):
|
|
89
|
+
# "Customer: Edit not turned on, because Read is off".
|
|
90
|
+
def pf_refusal_description(refusal)
|
|
91
|
+
pf_ui("changes.not_applied_#{refusal.needs}", resource: pf_resource_label(refusal.resource),
|
|
92
|
+
flag: pf_flag_name(refusal.attribute))
|
|
93
|
+
end
|
|
94
|
+
|
|
79
95
|
def pf_flag_name(flag)
|
|
80
96
|
pf_ui("flags_short.#{flag}")
|
|
81
97
|
end
|
|
@@ -99,15 +115,17 @@ module PermissionForge
|
|
|
99
115
|
%w[masked mask].include?(level) && strategy ? "#{text} (#{pf_mask_strategy_label(strategy)})" : text
|
|
100
116
|
end
|
|
101
117
|
|
|
118
|
+
# A field row named as everywhere else (Admin::FieldLabel: a label the
|
|
119
|
+
# gem generated is no label).
|
|
102
120
|
def pf_change_field_name(field)
|
|
103
|
-
field.
|
|
121
|
+
pf_field_label_text(field.resource&.name, field.name, row: field)
|
|
104
122
|
end
|
|
105
123
|
|
|
106
124
|
def pf_field_attr_change(change)
|
|
107
125
|
field = change.record
|
|
108
126
|
if change.attribute == 'is_active'
|
|
109
127
|
return pf_ui(change.to ? 'changes.field_on' : 'changes.field_off', resource: pf_resource_label(change.resource),
|
|
110
|
-
field: field
|
|
128
|
+
field: pf_change_field_name(field))
|
|
111
129
|
end
|
|
112
130
|
|
|
113
131
|
pf_ui('changes.field_text', resource: pf_resource_label(change.resource), field: field.name,
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
module PermissionForge
|
|
2
|
+
# Plain-language sentences for a Permission Set's activation conditions,
|
|
3
|
+
# read through ActivationConditions — the definition that judges them
|
|
4
|
+
# (round 7, E#32 / B#48a): the summary ("Only works from … between …"),
|
|
5
|
+
# its form on the set's own Conditions tab (`here:`), the stored IP
|
|
6
|
+
# entries marked as host data (`markup:`), the problems the validations
|
|
7
|
+
# refuse (#pf_condition_problem) and what such a condition does
|
|
8
|
+
# (#pf_condition_effect, shared with Home's card).
|
|
9
|
+
#
|
|
10
|
+
# @api private
|
|
11
|
+
module ConditionsHelper
|
|
12
|
+
# `here`: the sentence is drawn on the set's own Conditions tab, so a
|
|
13
|
+
# broken condition is not sent "to the Conditions tab" (E#32).
|
|
14
|
+
# `markup`: the stored IP entries in <code> (host data, marked); plain
|
|
15
|
+
# text for an attribute (a title).
|
|
16
|
+
def pf_condition_parts(permission_set, here: false, markup: false)
|
|
17
|
+
[pf_condition_ips(permission_set, here: here, markup: markup), pf_condition_hours(permission_set, here: here),
|
|
18
|
+
pf_condition_flag(permission_set)].compact
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# The IP entries as the engine reads them (ActivationConditions); a list
|
|
22
|
+
# it cannot judge (an entry that is no address, no entry at all) says so
|
|
23
|
+
# (B#48a), never reads as a list of addresses.
|
|
24
|
+
def pf_condition_ips(permission_set, here: false, markup: false)
|
|
25
|
+
conditions = PermissionForge::ActivationConditions.of(permission_set)
|
|
26
|
+
return if conditions.allowed_ip_list.empty? && !conditions.ip_list_broken?
|
|
27
|
+
|
|
28
|
+
key = if conditions.ip_list_broken? then here ? 'conditions.ips_broken_here' : 'conditions.ips_broken'
|
|
29
|
+
else 'conditions.from_ips'
|
|
30
|
+
end
|
|
31
|
+
listed = pf_condition_ip_list(conditions, permission_set, markup)
|
|
32
|
+
markup ? pf_t_markup(ips: listed) { |held| pf_ui(key, **held) } : pf_ui(key, ips: listed)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The entries as one list ("10.0.0.0/8 or office-network"), each in
|
|
36
|
+
# <code> with `markup`; without it (an attribute, which cannot mark host
|
|
37
|
+
# data) the stored text as written; a list of separators alone as written.
|
|
38
|
+
def pf_condition_ip_list(conditions, permission_set, markup)
|
|
39
|
+
return permission_set.allowed_ips.to_s.strip unless markup
|
|
40
|
+
|
|
41
|
+
ips = conditions.allowed_ip_list.presence || [permission_set.allowed_ips.to_s.strip]
|
|
42
|
+
pf_condition_code_list(ips.map { |ip| content_tag(:code, ip, class: 'pf-mono-key') })
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# "a, b or c" from markup, with the page's connectors.
|
|
46
|
+
def pf_condition_code_list(codes)
|
|
47
|
+
return codes.first if codes.one?
|
|
48
|
+
|
|
49
|
+
words = PermissionForge::Admin::ListSentence.connectors[:words_connector]
|
|
50
|
+
safe_join([safe_join(codes[0..-2], words), codes.last], pf_ui('conditions.or'))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The hour window as the engine reads it: none, a window, an overnight
|
|
54
|
+
# one, or a broken one (one hour of the two, the same hour twice, an
|
|
55
|
+
# hour outside 0–23) that nobody can judge — said so, never shown as an
|
|
56
|
+
# overnight window.
|
|
57
|
+
def pf_condition_hours(permission_set, here: false)
|
|
58
|
+
window = PermissionForge::ActivationConditions.of(permission_set).hours_window
|
|
59
|
+
return unless window.set?
|
|
60
|
+
|
|
61
|
+
hours = { from: pf_hour_or_blank(window.from), to: pf_hour_or_blank(window.to) }
|
|
62
|
+
return pf_ui(here ? 'conditions.hours_broken_here' : 'conditions.hours_broken', **hours) if window.broken?
|
|
63
|
+
|
|
64
|
+
pf_ui("conditions.#{window.wraps? ? 'hours_overnight' : 'hours'}", **hours)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def pf_hour_or_blank(hour)
|
|
68
|
+
hour.nil? ? pf_ui('conditions.no_hour') : pf_hour(hour)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# One sentence per ActivationConditions#condition_problems entry: what
|
|
72
|
+
# cannot be judged, the stored entry as written (in <code>).
|
|
73
|
+
def pf_condition_problem(kind, value)
|
|
74
|
+
case kind
|
|
75
|
+
when :ip_entry then pf_ui('conditions.problems.ip_entry_html', entry: content_tag(:code, value, class: 'pf-mono-key'))
|
|
76
|
+
when :flag_unusable then pf_ui('conditions.problems.flag_unusable_html', flag: content_tag(:code, value.inspect, class: 'pf-mono-key'))
|
|
77
|
+
else pf_ui("conditions.problems.#{kind}")
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# What a condition nobody can judge does to the set (ActivationConditions,
|
|
82
|
+
# ConditionalSets::Member): a granting set never passes, a muting set
|
|
83
|
+
# applies to every request — the words Home's card uses.
|
|
84
|
+
def pf_condition_effect(permission_set)
|
|
85
|
+
pf_ui("conditions.effect.#{permission_set.is_muting ? 'muting' : 'granting'}")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def pf_condition_flag(permission_set)
|
|
89
|
+
flag = permission_set.requires_session_flag
|
|
90
|
+
pf_flag_phrase(flag) if flag.present?
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# The set's conditions as one sentence; `markup` draws the stored IP
|
|
94
|
+
# entries in <code> (a page's text), without it plain text (a title).
|
|
95
|
+
def pf_condition_sentence(permission_set, here: false, markup: false)
|
|
96
|
+
parts = pf_condition_parts(permission_set, here: here, markup: markup)
|
|
97
|
+
return pf_ui('conditions.always') if parts.empty?
|
|
98
|
+
return pf_ui('conditions.only_works', parts: parts.join(', ')) unless markup
|
|
99
|
+
|
|
100
|
+
pf_t_markup(parts: safe_join(parts, ', ')) { |held| pf_ui('conditions.only_works', **held) }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# The entries of an IP list as written (ActivationConditions.entries).
|
|
104
|
+
def pf_ip_list(value)
|
|
105
|
+
PermissionForge::ActivationConditions.entries(value)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def pf_flag_label(flag)
|
|
109
|
+
configured = PermissionForge.configuration.admin_session_flags || {}
|
|
110
|
+
configured[flag.to_s] || configured[flag.to_sym] ||
|
|
111
|
+
t("permission_forge.ui.flags.#{flag}", default: flag.to_s)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def pf_flag_phrase(flag)
|
|
115
|
+
t("permission_forge.ui.conditions.flags.#{flag}",
|
|
116
|
+
default: pf_ui('conditions.flag_generic', flag: pf_flag_label(flag)))
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
module PermissionForge
|
|
2
|
+
# Labels of stored names in the page's language: a field, column or
|
|
3
|
+
# association name (Admin::FieldLabel), a model or Resource
|
|
4
|
+
# (Admin::ModelLabel), an action (Admin::ActionLabel) — the host's own
|
|
5
|
+
# translation when it has one, else the label an admin wrote, else (on an
|
|
6
|
+
# English page) Rails' English, else the name verbatim, drawn in <code>
|
|
7
|
+
# — and the names of sets, groups and rules (Admin::RecordName), what a
|
|
8
|
+
# form pre-fills (Admin::Prefill). Never Rails' English inflections on a
|
|
9
|
+
# page in another language (release_readiness/inflection_guard_test.rb).
|
|
10
|
+
#
|
|
11
|
+
# @api private
|
|
12
|
+
module LabelsHelper
|
|
13
|
+
# --- labels of stored names --------------------------------------------------
|
|
14
|
+
|
|
15
|
+
# The label of a stored field, column or association name
|
|
16
|
+
# (Admin::FieldLabel): the host's translation, else the row's label,
|
|
17
|
+
# else the name verbatim — then drawn in <code>, as a stored name is.
|
|
18
|
+
def pf_field_label(model, name, row: nil)
|
|
19
|
+
label = pf_field_label_for(model, name, row: row)
|
|
20
|
+
label.verbatim? ? tag.code(label.text, class: 'pf-mono-key') : label.text
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Text of the host (a label, a description, a stored value) drawn as it
|
|
24
|
+
# is, marked so the locale-leak test tells it from the gem's own words.
|
|
25
|
+
#
|
|
26
|
+
# @return [ActiveSupport::SafeBuffer]
|
|
27
|
+
def pf_host_data(text, tag_name = :span)
|
|
28
|
+
content_tag(tag_name, text, data: { pf_host_data: true })
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The same label as plain text (an attribute, a title, an option).
|
|
32
|
+
def pf_field_label_text(model, name, row: nil)
|
|
33
|
+
pf_field_label_for(model, name, row: row).text
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @return [Admin::FieldLabel::Label]
|
|
37
|
+
def pf_field_label_for(model, name, row: nil)
|
|
38
|
+
PermissionForge::Admin::FieldLabel.for(model, name, row: row)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The label of an action (Admin::ActionLabel): the row's label, else the
|
|
42
|
+
# key verbatim, as text.
|
|
43
|
+
def pf_action_label_text(action, key = action&.name)
|
|
44
|
+
PermissionForge::Admin::ActionLabel.for(action, key).text
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# The plural label of a model (Admin::ModelLabel): the host's plural
|
|
48
|
+
# translation, else "<label> records" in the page's language.
|
|
49
|
+
def pf_resource_plural(name)
|
|
50
|
+
PermissionForge::Admin::ModelLabel.plural(name)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The label of a Resource or model name (Admin::ModelLabel): the row's
|
|
54
|
+
# label, else the host's translation, else the constant's name.
|
|
55
|
+
def pf_resource_label(resource_or_name)
|
|
56
|
+
PermissionForge::Admin::ModelLabel.for(resource_or_name).text
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The name of a Permission Set, group or rule (Admin::RecordName): as
|
|
60
|
+
# stored, except an install-default English name, named in the page's
|
|
61
|
+
# language.
|
|
62
|
+
def pf_record_name(record)
|
|
63
|
+
PermissionForge::Admin::RecordName.for(record)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# A stored name of `kind` without its record (a list a service answered,
|
|
67
|
+
# an event's label): Admin::RecordName.text.
|
|
68
|
+
def pf_record_name_text(name, kind)
|
|
69
|
+
PermissionForge::Admin::RecordName.text(name, kind)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# What a form's text field shows for a stored attribute (Admin::Prefill):
|
|
73
|
+
# never a column default of a new record, nor text the gem generated.
|
|
74
|
+
def pf_prefill(record, attribute)
|
|
75
|
+
record ? PermissionForge::Admin::Prefill.text(record, attribute) : ''
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Whether a required text field may be sent blank: the stored value is
|
|
79
|
+
# generated text the form shows as blank (#pf_prefill), and a blank
|
|
80
|
+
# posted over it is no change (Admin::Prefill.posted). Such a field
|
|
81
|
+
# drops `required`, so a page saves with and without JavaScript alike.
|
|
82
|
+
def pf_prefill_optional?(record, attribute)
|
|
83
|
+
return false unless record&.persisted?
|
|
84
|
+
|
|
85
|
+
record.public_send(attribute).to_s.present? && pf_prefill(record, attribute).empty?
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# A Permission Set named on a row about one of its Resources: "<set> ·
|
|
89
|
+
# <Model>" when the set holds more than one Resource of `chain` (a
|
|
90
|
+
# single-table-inheritance base and its subclass: Admin::ChainEntries),
|
|
91
|
+
# the model marked as host data; the set's name alone otherwise (round 7,
|
|
92
|
+
# B#35).
|
|
93
|
+
#
|
|
94
|
+
# @param set [PermissionSet]
|
|
95
|
+
# @param resource_name [String] the Resource the row is about
|
|
96
|
+
# @param chain [Array<String>] the Resource names of the chain the list is about
|
|
97
|
+
def pf_set_entry_name(set, resource_name, chain)
|
|
98
|
+
name = pf_record_name(set)
|
|
99
|
+
return name unless PermissionForge::Admin::ChainEntries.several?(set, chain)
|
|
100
|
+
|
|
101
|
+
safe_join([name, ' · ', pf_host_data(pf_resource_label(resource_name))])
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# #pf_set_entry_name on a row about records of `leaf_name` (a person's
|
|
105
|
+
# effective access): the chain is the leaf's ResourceChain; `entry_name`
|
|
106
|
+
# the Resource the row's set acts through (an STI base whose entry
|
|
107
|
+
# decides, or the leaf itself).
|
|
108
|
+
def pf_chain_set_name(set, leaf_name, entry_name = leaf_name)
|
|
109
|
+
pf_set_entry_name(set, entry_name, pf_chain_names(leaf_name))
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# #pf_chain_set_name for an EffectiveAccess::FieldSource (a set that
|
|
113
|
+
# grants or mutes a field): the entry named is the Resource whose row
|
|
114
|
+
# the source credits (FieldSource#resource_name) — the deciding entry's
|
|
115
|
+
# for a grant, the muting row's for a mute. The row, the Why panel and
|
|
116
|
+
# "Explain for" all name a field's sets through here (B#38).
|
|
117
|
+
def pf_field_source_set_name(source, leaf_name)
|
|
118
|
+
pf_chain_set_name(source.permission_set, leaf_name, source.resource_name || leaf_name)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# #pf_chain_set_name for a Muting Permission Set that blocks a flag or an
|
|
122
|
+
# action (an EffectiveAccess::Outcome): the entry named is the Resource
|
|
123
|
+
# whose row blocks it (Outcome#muted_on).
|
|
124
|
+
def pf_muting_set_name(set, outcome, leaf_name)
|
|
125
|
+
pf_chain_set_name(set, leaf_name, outcome.muted_on.fetch(set, leaf_name))
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# The Resource names of the leaf's ResourceChain (memoized per page).
|
|
129
|
+
def pf_chain_names(leaf_name)
|
|
130
|
+
@pf_chains ||= {}
|
|
131
|
+
@pf_chains[leaf_name.to_s] ||= PermissionForge::ResourceChain.names_for(leaf_name).map(&:to_s)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -9,7 +9,7 @@ module PermissionForge
|
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
def pf_person_name(user)
|
|
12
|
-
user ? pf_person(user).name :
|
|
12
|
+
user ? pf_person(user).name : PermissionForge::Admin::ActorLabel.system
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def pf_avatar(user, size: nil)
|
|
@@ -30,13 +30,16 @@ module PermissionForge
|
|
|
30
30
|
|
|
31
31
|
# --- assignments --------------------------------------------------------
|
|
32
32
|
|
|
33
|
-
# [text, css tone class] for an assignment end date.
|
|
33
|
+
# [text, css tone class] for an assignment end date. "Ended" and "ends
|
|
34
|
+
# in" are PermissionSetUser's (.ended?, ENDING_SOON_DAYS — the window of
|
|
35
|
+
# Home's "ending soon" card and the People page filter).
|
|
34
36
|
def pf_expiry(expires_at)
|
|
35
37
|
days = PermissionForge::Admin::ExpiryPresets.days_left(expires_at)
|
|
38
|
+
assignments = PermissionForge::PermissionSetUser
|
|
36
39
|
return [pf_ui('expiry.none'), 'pf-text-2'] if days.nil?
|
|
37
|
-
return [pf_ui('expiry.expired', date: pf_date(expires_at, year: false)), 'pf-text-3'] if expires_at
|
|
40
|
+
return [pf_ui('expiry.expired', date: pf_date(expires_at, year: false)), 'pf-text-3'] if assignments.ended?(expires_at)
|
|
38
41
|
return [pf_ui('expiry.today'), 'pf-text-warn'] if days.zero?
|
|
39
|
-
return [pf_ui('expiry.ends_in', count: days, date: pf_date(expires_at, year: false)), 'pf-text-warn'] if days <=
|
|
42
|
+
return [pf_ui('expiry.ends_in', count: days, date: pf_date(expires_at, year: false)), 'pf-text-warn'] if days <= assignments::ENDING_SOON_DAYS
|
|
40
43
|
|
|
41
44
|
[pf_ui('expiry.until', date: pf_date(expires_at, year: false)), 'pf-text-2']
|
|
42
45
|
end
|
|
@@ -91,22 +94,11 @@ module PermissionForge
|
|
|
91
94
|
@pf_reviewers[id.to_s] = PermissionForge.configuration.user_model.find_by(id: id)
|
|
92
95
|
end
|
|
93
96
|
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
#
|
|
97
|
-
# over the assignments of the page (events: %w[assignment.granted]).
|
|
98
|
-
def pf_granted_by(assignment, granters = nil)
|
|
99
|
-
event = granters && granters[assignment]
|
|
100
|
-
event ? pf_audit_actor_name(event) : pf_person_name(assignment.try(:created_by))
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def pf_granters(assignments)
|
|
104
|
-
PermissionForge::Admin::LastChanges.new(assignments, events: %w[assignment.granted])
|
|
105
|
-
end
|
|
106
|
-
|
|
97
|
+
# Due for review: PermissionSetUser#stale? at the admin's threshold
|
|
98
|
+
# (Admin::AttentionItems.stale_days — Home's Stale card and the audit's
|
|
99
|
+
# stale_assignments use the same one, also while the setting is nil).
|
|
107
100
|
def pf_stale?(assignment)
|
|
108
|
-
assignment.
|
|
109
|
-
assignment.last_reviewed_at < PermissionForge.configuration.admin_stale_assignment_days.to_i.days.ago
|
|
101
|
+
assignment.stale?(PermissionForge::Admin::AttentionItems.stale_days)
|
|
110
102
|
end
|
|
111
103
|
|
|
112
104
|
# Calendar date (not time) for each expiry preset, for labels.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
module PermissionForge
|
|
2
|
+
# How the admin draws a record rule that matches nothing (Operators::RuleProblem,
|
|
3
|
+
# the one taxonomy the audit's rule check and Home share): a state with a
|
|
4
|
+
# glyph and a word, and why — never the rule's sentence, which would read
|
|
5
|
+
# like a rule that works. SentencesHelper asks here first.
|
|
6
|
+
#
|
|
7
|
+
# @api private
|
|
8
|
+
module RuleProblemsHelper
|
|
9
|
+
# Why the rule matches nothing (Operators::RuleProblem, the definition
|
|
10
|
+
# the audit and Home share), or nil. Asked once per rule object.
|
|
11
|
+
def pf_rule_problem(rule)
|
|
12
|
+
@pf_rule_problems ||= {}.compare_by_identity
|
|
13
|
+
return @pf_rule_problems[rule] if @pf_rule_problems.key?(rule)
|
|
14
|
+
|
|
15
|
+
@pf_rule_problems[rule] = PermissionForge::Operators::RuleProblem.for(rule)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# "`stauts` is not a column of Customer": a name that is not there as
|
|
19
|
+
# stored (RuleProblem#verbatim?: `onwer_id`, `ownr`, `regex`, `nosuch`),
|
|
20
|
+
# so the admin recognises the typo — humanized it would read like a real
|
|
21
|
+
# field; a real column or relationship by its label; a value as stored,
|
|
22
|
+
# in code, next to its column's label. The sentence is the problem's
|
|
23
|
+
# (RuleProblem#ui_key). The model may be none this app can load.
|
|
24
|
+
def pf_rule_problem_text(problem)
|
|
25
|
+
model = problem.model
|
|
26
|
+
args = { name: pf_rule_problem_name(problem), model: (pf_resource_label(model.name) if model) }
|
|
27
|
+
args[:value] = content_tag(:code, problem.value, class: 'pf-mono-key') if problem.value
|
|
28
|
+
pf_ui("rules.problems.#{problem.ui_key}", **args)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def pf_rule_problem_name(problem)
|
|
32
|
+
return content_tag(:code, problem.name, class: 'pf-mono-key') if problem.verbatim?
|
|
33
|
+
|
|
34
|
+
problem.model ? pf_attribute_label(problem.model, problem.name) : problem.name
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# "Matches nothing: `stauts` is not a column of Customer"
|
|
38
|
+
def pf_rule_problem_line(problem)
|
|
39
|
+
safe_join([pf_ui('rules.matches_nothing'), ': ', pf_rule_problem_text(problem)])
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# The rule a one-line summary shows: one that matches nothing first.
|
|
43
|
+
def pf_rule_headline(rules)
|
|
44
|
+
rules.partition { |rule| pf_rule_problem(rule) }.flatten.first
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# A badge per note on a stored rule (Admin::RuleNotes, most severe
|
|
48
|
+
# first): "⊘ Matches nothing", "⚠ Secret column" — the Rules tab's row.
|
|
49
|
+
def pf_rule_note_badges(rule)
|
|
50
|
+
notes = PermissionForge::Admin::RuleNotes.for(rule)
|
|
51
|
+
safe_join(notes.map do |note|
|
|
52
|
+
word = note.matches_nothing? ? "⊘ #{pf_ui('rules.matches_nothing')}" : "⚠ #{pf_ui("rules.notes.#{note.kind}")}"
|
|
53
|
+
content_tag(:span, pf_badge(word, variant: note.matches_nothing? ? :block : :warn), data: { pf_rule_note: note.kind })
|
|
54
|
+
end, ' ')
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# ◷ for a rule, ⊘ for one that matches nothing.
|
|
58
|
+
def pf_rule_glyph(rule)
|
|
59
|
+
pf_rule_problem(rule) ? '⊘' : '◷'
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# [⊘ Matches nothing] Stauts is not a column of Customer `stauts equal active`
|
|
63
|
+
def pf_rule_problem_sentence(rule, problem)
|
|
64
|
+
operator = rule.operator
|
|
65
|
+
value = rule.field_value unless PermissionForge::Operators::Validations.valueless?(operator)
|
|
66
|
+
written = [PermissionForge::Operators::RuleProblem.written_field(rule), operator, value]
|
|
67
|
+
content_tag(:p, safe_join([pf_badge("⊘ #{pf_ui('rules.matches_nothing')}", variant: :block),
|
|
68
|
+
content_tag(:span, pf_rule_problem_text(problem)),
|
|
69
|
+
content_tag(:code, written.compact.join(' '), class: 'pf-mono-key')], ' '),
|
|
70
|
+
class: 'pf-sentence')
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|