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
data/config/routes.rb
CHANGED
|
@@ -85,6 +85,7 @@ PermissionForge::Engine.routes.draw do
|
|
|
85
85
|
resources :rules, controller: 'permission_set_rules', only: %i[new create edit update destroy] do
|
|
86
86
|
collection do
|
|
87
87
|
post :refresh
|
|
88
|
+
get :preview
|
|
88
89
|
end
|
|
89
90
|
member do
|
|
90
91
|
patch :toggle
|
|
@@ -107,37 +108,51 @@ PermissionForge::Engine.routes.draw do
|
|
|
107
108
|
# JSON Admin API — routes are always drawn; access is guarded at runtime
|
|
108
109
|
# by BaseController checking api_enabled + api_auth configuration.
|
|
109
110
|
namespace :api, defaults: { format: :json } do
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
#
|
|
113
|
-
|
|
111
|
+
# JSON only: `.json` or no extension. Any other extension
|
|
112
|
+
# (`/permission_sets.xml`) matches no endpoint and reaches the catch-all
|
|
113
|
+
# below: the JSON 404 envelope, never a JSON 200 under another format.
|
|
114
|
+
constraints(format: /json/) do
|
|
115
|
+
resource :stats, only: [:show], controller: 'dashboard'
|
|
116
|
+
|
|
117
|
+
# The audit log is read-only over the API: no write route exists.
|
|
118
|
+
resources :audit_events, only: %i[index show], param: :identifier
|
|
119
|
+
|
|
120
|
+
# No new/edit: a JSON API has no forms. Without them `…/new` reads as
|
|
121
|
+
# an identifier (404 unknown record) and `…/:identifier/edit` reaches
|
|
122
|
+
# the catch-all (404), both in the envelope; through 0.14.1 they raised
|
|
123
|
+
# AbstractController::ActionNotFound (Rails' own 404 page).
|
|
124
|
+
resources :permission_sets, param: :identifier, except: %i[new edit] do
|
|
125
|
+
member do
|
|
126
|
+
post :clone
|
|
127
|
+
end
|
|
114
128
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
129
|
+
resources :resources, param: :identifier, except: %i[new edit] do
|
|
130
|
+
member do
|
|
131
|
+
post :generate_fields
|
|
132
|
+
end
|
|
133
|
+
resources :fields, param: :identifier, controller: 'resource_fields', except: %i[new edit]
|
|
134
|
+
resources :actions, param: :identifier, controller: 'resource_actions', except: %i[new edit]
|
|
135
|
+
resources :rules, param: :identifier, controller: 'permission_set_rules', except: %i[new edit]
|
|
136
|
+
end
|
|
119
137
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
138
|
+
resources :users, param: :identifier, controller: 'permission_set_users', only: %i[index create destroy] do
|
|
139
|
+
collection do
|
|
140
|
+
post :bulk
|
|
141
|
+
end
|
|
123
142
|
end
|
|
124
|
-
resources :fields, param: :identifier, controller: 'resource_fields'
|
|
125
|
-
resources :actions, param: :identifier, controller: 'resource_actions'
|
|
126
|
-
resources :rules, param: :identifier, controller: 'permission_set_rules'
|
|
127
143
|
end
|
|
128
144
|
|
|
129
|
-
resources :
|
|
130
|
-
|
|
131
|
-
post :
|
|
145
|
+
resources :permission_set_groups, param: :identifier, except: %i[new edit] do
|
|
146
|
+
member do
|
|
147
|
+
post :clone
|
|
132
148
|
end
|
|
149
|
+
resources :users, param: :identifier, controller: 'permission_set_group_users', only: %i[index create destroy]
|
|
133
150
|
end
|
|
134
151
|
end
|
|
135
152
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
resources :users, param: :identifier, controller: 'permission_set_group_users', only: %i[index create destroy]
|
|
141
|
-
end
|
|
153
|
+
# Last: any other path or method under /api answers the JSON 404
|
|
154
|
+
# envelope, not Rails' HTML page (Api::UnmatchedRoutesController) —
|
|
155
|
+
# OPTIONS and TRACE included (through 0.15.0-rc a RoutingError).
|
|
156
|
+
match '(*path)', to: 'unmatched_routes#show', via: %i[get post put patch delete options trace], format: false
|
|
142
157
|
end
|
|
143
158
|
end
|
|
@@ -23,6 +23,16 @@ module PermissionForge
|
|
|
23
23
|
|
|
24
24
|
KEY_TYPES = %w[bigint integer uuid string].freeze
|
|
25
25
|
DETECTED_KEY_TYPES = { integer: 'bigint', uuid: 'uuid', string: 'string' }.freeze
|
|
26
|
+
# Printed after an install (#print_next_steps).
|
|
27
|
+
NEXT_STEPS = [
|
|
28
|
+
'bin/rails db:migrate',
|
|
29
|
+
'In your User model: include PermissionForge::Concerns::PermissionedUser (can_read?, permissions_hash)',
|
|
30
|
+
'Mount the admin in config/routes.rb: mount PermissionForge::Engine, at: "/permissions"',
|
|
31
|
+
"Admin credentials: uncomment Option A in config/initializers/permission_forge.rb\n " \
|
|
32
|
+
'(PF_ADMIN_USER, PF_ADMIN_PASSWORD), or use your own admin controller (Option B)',
|
|
33
|
+
'bin/rails permission_forge:audit lists what is still open before production'
|
|
34
|
+
].freeze
|
|
35
|
+
QUICK_START_URL = 'https://github.com/fetijashari/permission_forge#quick-start'.freeze
|
|
26
36
|
|
|
27
37
|
def copy_initializer
|
|
28
38
|
template 'initializer.rb.tt', 'config/initializers/permission_forge.rb'
|
|
@@ -34,6 +44,17 @@ module PermissionForge
|
|
|
34
44
|
migration_template 'migration.rb.tt', 'db/migrate/create_permission_forge_tables.rb'
|
|
35
45
|
end
|
|
36
46
|
|
|
47
|
+
# What the files alone do not do: without a mount and credentials there
|
|
48
|
+
# is no admin to open. Not printed by `rails destroy`.
|
|
49
|
+
def print_next_steps
|
|
50
|
+
return unless behavior == :invoke
|
|
51
|
+
|
|
52
|
+
steps = NEXT_STEPS.drop(options[:skip_migration] ? 1 : 0)
|
|
53
|
+
say "\nPermissionForge is installed. Next steps:"
|
|
54
|
+
steps.each.with_index(1) { |step, number| say " #{number}. #{step}" }
|
|
55
|
+
say " Quick Start: #{QUICK_START_URL}"
|
|
56
|
+
end
|
|
57
|
+
|
|
37
58
|
private
|
|
38
59
|
|
|
39
60
|
def table_prefix
|
|
@@ -10,23 +10,23 @@ PermissionForge.configure do |config|
|
|
|
10
10
|
# ---------------------------------------------------------------------------
|
|
11
11
|
# A guarded controller action (ControllerActionGuard) with nobody signed in is refused.
|
|
12
12
|
config.action_guard_nil_user_strategy = :deny
|
|
13
|
+
# So, by default, is any guarded action no Permission Set manages (opt out: config.unconfigured_action_strategy = :allow).
|
|
13
14
|
# Conditional Permission Sets fail closed without a request context (see below).
|
|
14
15
|
config.conditional_sets_strict_mode = true
|
|
15
|
-
#
|
|
16
|
+
# Rails.cache, shared by every process: Redis or Solid Cache in production, :file_store (not :memory_store) in development.
|
|
16
17
|
config.cache_backend = :rails_cache
|
|
17
18
|
|
|
18
|
-
# config.preset = :strict closes
|
|
19
|
-
#
|
|
20
|
-
# nil_user_strategy = :deny, auto_query_scoping = :filter_only,
|
|
21
|
-
# response_filter_nil_user_strategy = :block,
|
|
22
|
-
#
|
|
23
|
-
# and the three settings above.
|
|
19
|
+
# config.preset = :strict closes the fail-open defaults a setting can close (masking with
|
|
20
|
+
# nobody signed in too: :hide_in_requests, set in its own section below):
|
|
21
|
+
# nil_user_strategy = :deny, auto_query_scoping = :filter_only, no_read_access_strategy = :deny,
|
|
22
|
+
# response_filter_nil_user_strategy = :block, mask_virtual_attributes = :auto,
|
|
23
|
+
# extra_fields_default_access = :unconfigured, and the three settings above. A setting you make here still wins (docs/configuration.md#presets).
|
|
24
24
|
# What it costs: with nil_user_strategy :deny and auto_query_scoping :filter_only,
|
|
25
|
-
# code that runs with no current user (jobs, mailers, seeds, webhooks, the
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
# fails ("must exist"). Run such code
|
|
29
|
-
# PermissionForge.without_permission_scope { }, where nobody signed in gets no rules:
|
|
25
|
+
# code that runs with no current user (jobs, mailers, seeds, webhooks, the console,
|
|
26
|
+
# sign-in lookups on a user model that includes PermissionScopedFindable) sees no
|
|
27
|
+
# protected rows (a seed's find_or_create_by! adds a duplicate on every run), and a
|
|
28
|
+
# belongs_to presence check on a protected parent fails ("must exist"). Run such code
|
|
29
|
+
# with a user, or inside PermissionForge.without_permission_scope { }, where nobody signed in gets no rules:
|
|
30
30
|
# never around code an anonymous request reaches. Never include PermissionScopedFindable in the user model.
|
|
31
31
|
# config.preset = :strict
|
|
32
32
|
|
|
@@ -48,10 +48,9 @@ PermissionForge.configure do |config|
|
|
|
48
48
|
# reports the assignments still held by users it calls inactive.
|
|
49
49
|
config.user_active_check = ->(user) { user.respond_to?(:active_for_authentication?) ? user.active_for_authentication? : true }
|
|
50
50
|
|
|
51
|
-
# The profile cached with each user's permissions
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
51
|
+
# The profile cached with each user's permissions. The default is like this example but leaves an encrypted
|
|
52
|
+
# attribute out (nil); this lambda calls the public readers, which decrypt (and may be masked):
|
|
53
|
+
# config.profile_builder = ->(user) { { id: user.id, uuid: user.try(:uuid), name: user.try(:name), email: user.try(:email).presence || user.try(:email_address) } }
|
|
55
54
|
|
|
56
55
|
# Prefix of the PermissionForge tables ('' for none).
|
|
57
56
|
config.table_name_prefix = '<%= table_prefix %>'
|
|
@@ -85,14 +84,15 @@ PermissionForge.configure do |config|
|
|
|
85
84
|
# keep it cheap, never read permissions inside it, return {} with no request.
|
|
86
85
|
|
|
87
86
|
# ---------------------------------------------------------------------------
|
|
88
|
-
# Masked attributes with nobody signed in
|
|
87
|
+
# Masked attributes with nobody signed in
|
|
89
88
|
# ---------------------------------------------------------------------------
|
|
90
|
-
#
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
89
|
+
# Hidden in a web request with nobody signed in (a public page, a forgotten
|
|
90
|
+
# sign-in, a before_action before it, a mail sent with deliver_now); jobs, rake
|
|
91
|
+
# and the console keep stored values. :hide hides in jobs too; :bypass nowhere.
|
|
92
|
+
# Webhooks that normalize records: sign a system user in (a value derived from
|
|
93
|
+
# a hidden read can still be saved into another column).
|
|
94
|
+
# The user model stays readable for sign-in; validations read stored values.
|
|
95
|
+
config.masked_attributes_nil_user_strategy = :hide_in_requests
|
|
96
96
|
|
|
97
97
|
# ---------------------------------------------------------------------------
|
|
98
98
|
# Admin screens
|
|
@@ -108,15 +108,15 @@ PermissionForge.configure do |config|
|
|
|
108
108
|
# config.http_basic_auth_enabled = false
|
|
109
109
|
# config.base_controller_class = '::Admin::BaseController' # must authenticate admins
|
|
110
110
|
#
|
|
111
|
-
# Who may grant, revoke, extend or review an assignment, or change a group's
|
|
112
|
-
#
|
|
111
|
+
# Who may grant, revoke, extend or review an assignment, or change a group's members
|
|
112
|
+
# (admin screens and API). Example: no self-assignment. PermissionForge.current_user is
|
|
113
|
+
# current_user_resolver's user; under Option A your app's sign-in does not run (nil).
|
|
113
114
|
# config.admin_assignment_authorization = ->(controller, target, user_ids, action) {
|
|
114
|
-
# admin =
|
|
115
|
+
# admin = PermissionForge.current_user
|
|
115
116
|
# admin&.admin? && !user_ids.include?(admin.id)
|
|
116
117
|
# }
|
|
117
|
-
#
|
|
118
118
|
# Who may read the audit log pages, exports and API (nil: anyone with admin access).
|
|
119
|
-
# config.audit_log_authorization = ->(controller) {
|
|
119
|
+
# config.audit_log_authorization = ->(controller) { PermissionForge.current_user&.auditor? }
|
|
120
120
|
|
|
121
121
|
# ---------------------------------------------------------------------------
|
|
122
122
|
# Audit log (docs/audit-log.md)
|
|
@@ -124,7 +124,8 @@ PermissionForge.configure do |config|
|
|
|
124
124
|
# Every permission change is recorded in the audit_events table, inside the
|
|
125
125
|
# change's own transaction. Each option with its default (an example where that is nil):
|
|
126
126
|
# config.audit_events = :auto # true: always; false: never (events are still published)
|
|
127
|
-
# config.audit_actor_resolver = ->(controller) { controller.
|
|
127
|
+
# config.audit_actor_resolver = ->(controller) { Session.find_by(id: controller.request.cookie_jar.signed[:session_id])&.user }
|
|
128
|
+
# nil: current_user_resolver, then the Basic auth login; the example names a Rails 8 sign-in under Option A
|
|
128
129
|
# config.audit_request_context = ->(controller) { { request_id: controller.request.request_id, ip: controller.request.remote_ip } }
|
|
129
130
|
# config.audit_change_failure_strategy = :raise # the change rolls back; or :report, :log
|
|
130
131
|
# config.audit_hook_failure_strategy = :report # or :log, :raise
|
|
@@ -138,11 +139,11 @@ PermissionForge.configure do |config|
|
|
|
138
139
|
# ---------------------------------------------------------------------------
|
|
139
140
|
# config.api_enabled = true
|
|
140
141
|
# config.api_base_controller_class = '::Api::BaseController' # your auth chain
|
|
141
|
-
# config.api_auth = ->(controller) {
|
|
142
|
-
# config.api_action_authorization = ->(controller, action, record) {
|
|
143
|
-
# config.api_assignment_authorization = ->(controller, permission_set, user_ids) {
|
|
142
|
+
# config.api_auth = ->(controller) { PermissionForge.current_user&.admin? }
|
|
143
|
+
# config.api_action_authorization = ->(controller, action, record) { PermissionForge.current_user&.admin? }
|
|
144
|
+
# config.api_assignment_authorization = ->(controller, permission_set, user_ids) { PermissionForge.current_user&.admin? }
|
|
144
145
|
#
|
|
145
|
-
#
|
|
146
|
-
#
|
|
146
|
+
# config.api_stats_authorization = ->(controller) { PermissionForge.current_user&.admin? } # GET /api/stats
|
|
147
|
+
# api_auth proves identity only: until each lambda above is set, what it guards is refused (403).
|
|
147
148
|
# config.api_require_authorization = true # default
|
|
148
149
|
end
|
|
@@ -2,7 +2,7 @@ class CreatePermissionForgeTables < ActiveRecord::Migration<%= migration_version
|
|
|
2
2
|
def change
|
|
3
3
|
create_table :<%= table_prefix %>permission_set_groups do |t|
|
|
4
4
|
t.string :uuid, null: false
|
|
5
|
-
t.string :name, null: false
|
|
5
|
+
t.string :name, null: false
|
|
6
6
|
t.string :description
|
|
7
7
|
t.boolean :is_active, null: false, default: true
|
|
8
8
|
t.boolean :is_editable, null: false, default: true
|
|
@@ -18,7 +18,7 @@ class CreatePermissionForgeTables < ActiveRecord::Migration<%= migration_version
|
|
|
18
18
|
create_table :<%= table_prefix %>permission_sets do |t|
|
|
19
19
|
t.string :uuid, null: false
|
|
20
20
|
t.references :permission_set_group, foreign_key: { to_table: :<%= table_prefix %>permission_set_groups }, null: true
|
|
21
|
-
t.string :name, null: false
|
|
21
|
+
t.string :name, null: false
|
|
22
22
|
t.string :description
|
|
23
23
|
t.boolean :is_active, null: false, default: true
|
|
24
24
|
t.boolean :is_editable, null: false, default: true
|
|
@@ -106,14 +106,14 @@ class CreatePermissionForgeTables < ActiveRecord::Migration<%= migration_version
|
|
|
106
106
|
t.string :uuid, null: false
|
|
107
107
|
t.references :permission_set, foreign_key: { to_table: :<%= table_prefix %>permission_sets }, null: false
|
|
108
108
|
t.references :resource, foreign_key: { to_table: :<%= table_prefix %>resources }, null: false
|
|
109
|
-
t.string :name, null: false
|
|
110
|
-
t.string :failure_message
|
|
109
|
+
t.string :name, null: false
|
|
110
|
+
t.string :failure_message
|
|
111
111
|
t.string :join_type
|
|
112
112
|
t.string :field_name, null: false
|
|
113
113
|
t.string :field_value, null: false
|
|
114
114
|
t.string :field_accessor_path
|
|
115
115
|
t.string :operator, null: false, default: 'equal'
|
|
116
|
-
t.boolean :include_nulls, null: false, default:
|
|
116
|
+
t.boolean :include_nulls, null: false, default: false
|
|
117
117
|
t.boolean :is_active, null: false, default: true
|
|
118
118
|
t.boolean :is_editable, null: false, default: true
|
|
119
119
|
<% if tenant_column %> t.<%= tenant_id_type %> :<%= tenant_column %>
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ipaddr'
|
|
4
|
+
|
|
5
|
+
module PermissionForge
|
|
6
|
+
# The activation conditions of a Permission Set as its row holds them: the
|
|
7
|
+
# one reading of `allowed_ips` (a list of IP addresses or CIDR ranges
|
|
8
|
+
# written with commas, newlines or spaces between them) and of the hour
|
|
9
|
+
# window (`allowed_hours_start`, `allowed_hours_end`). Included in
|
|
10
|
+
# PermissionSet (the row) and in ConditionalSets::Member (what the cache
|
|
11
|
+
# keeps of a conditional set); anything else that answers the three
|
|
12
|
+
# readers is read through {.of}.
|
|
13
|
+
#
|
|
14
|
+
# Who reads it: Services::PermissionSetConditionEvaluator (every
|
|
15
|
+
# per-request selection), the PermissionSet validations (a new row, or a
|
|
16
|
+
# changed condition, is refused when it cannot be judged — the stored text
|
|
17
|
+
# is normalised to commas), Admin::ConditionsForm, SentencesHelper, the
|
|
18
|
+
# API serializer and the `conditions` security-audit check.
|
|
19
|
+
#
|
|
20
|
+
# A condition nobody can judge — an entry that is not an address, a list
|
|
21
|
+
# of separators alone, one hour of the two, the same hour twice, an hour
|
|
22
|
+
# outside 0..23 — is *broken* (#conditions_broken?): a granting set never
|
|
23
|
+
# passes on it, with or without a request context, and a muting set always
|
|
24
|
+
# applies (ConditionalSets::Member), in every mode. The condition can only be written past the model.
|
|
25
|
+
#
|
|
26
|
+
# @api private
|
|
27
|
+
module ActivationConditions
|
|
28
|
+
# What separates two entries of an IP list.
|
|
29
|
+
SEPARATORS = /[,\s]+/
|
|
30
|
+
# The hours of a day.
|
|
31
|
+
HOURS = (0..23)
|
|
32
|
+
|
|
33
|
+
# The hour window of a set: none (#set? false), a window from `from`
|
|
34
|
+
# (inclusive) to `to` (exclusive) that may wrap midnight (#wraps?), or a
|
|
35
|
+
# broken one (#broken?: one hour missing, an hour outside HOURS, or
|
|
36
|
+
# `from == to`) that includes no hour.
|
|
37
|
+
#
|
|
38
|
+
# @api private
|
|
39
|
+
HoursWindow = Data.define(:from, :to) do
|
|
40
|
+
def set? = !(from.nil? && to.nil?)
|
|
41
|
+
def complete? = !from.nil? && !to.nil?
|
|
42
|
+
def in_range? = [from, to].compact.all? { |hour| HOURS.cover?(hour) }
|
|
43
|
+
def broken? = set? && !problem.nil?
|
|
44
|
+
def wraps? = complete? && from > to
|
|
45
|
+
|
|
46
|
+
# Why the window cannot be judged: :hours_incomplete, :hours_range,
|
|
47
|
+
# :hours_equal — nil for a window that can be (or none at all).
|
|
48
|
+
#
|
|
49
|
+
# @return [Symbol, nil]
|
|
50
|
+
def problem
|
|
51
|
+
return unless set?
|
|
52
|
+
return :hours_incomplete unless complete?
|
|
53
|
+
return :hours_range unless in_range?
|
|
54
|
+
|
|
55
|
+
:hours_equal if from == to
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @param hour [Integer] 0..23
|
|
59
|
+
# @return [Boolean] whether the window includes the hour; never for a
|
|
60
|
+
# broken window or none at all
|
|
61
|
+
def include?(hour)
|
|
62
|
+
return false unless set? && !broken?
|
|
63
|
+
|
|
64
|
+
wraps? ? hour >= from || hour < to : hour >= from && hour < to
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# The conditions of an object that answers the three readers (a host's
|
|
69
|
+
# own object, a Struct) without including the module.
|
|
70
|
+
#
|
|
71
|
+
# @api private
|
|
72
|
+
Reading = Data.define(:allowed_ips, :allowed_hours_start, :allowed_hours_end) { include ActivationConditions }
|
|
73
|
+
|
|
74
|
+
class << self
|
|
75
|
+
# @param object [#allowed_ips, #allowed_hours_start, #allowed_hours_end]
|
|
76
|
+
# @return [ActivationConditions]
|
|
77
|
+
def of(object)
|
|
78
|
+
return object if object.is_a?(ActivationConditions)
|
|
79
|
+
|
|
80
|
+
Reading.new(allowed_ips: object.allowed_ips, allowed_hours_start: object.allowed_hours_start,
|
|
81
|
+
allowed_hours_end: object.allowed_hours_end)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# The entries of an IP list as written: a String with commas, newlines
|
|
85
|
+
# or spaces between the entries, or an Array of such Strings (the API
|
|
86
|
+
# takes both). Blanks are dropped; the entries are not checked.
|
|
87
|
+
#
|
|
88
|
+
# @param value [String, Array<String>, nil]
|
|
89
|
+
# @return [Array<String>]
|
|
90
|
+
def entries(value)
|
|
91
|
+
Array(value).flat_map { |part| part.to_s.split(SEPARATORS) }.reject(&:empty?)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# A condition text as a row stores it: nil for text of whitespace
|
|
95
|
+
# alone — any Unicode space, which Ruby reads as blank while SQL's TRIM
|
|
96
|
+
# takes spaces only — so #conditional? and the SQL scopes read it the
|
|
97
|
+
# same. A row stored before keeps its text; SQL reads it as a
|
|
98
|
+
# condition (narrower: the set is selected per request and passes).
|
|
99
|
+
#
|
|
100
|
+
# @param value [String, nil]
|
|
101
|
+
# @return [String, nil]
|
|
102
|
+
def condition_text(value)
|
|
103
|
+
value.is_a?(String) && value.match?(/\A[[:space:]]*\z/) ? nil : value
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# The address of a request, as the IP list is compared with it: an
|
|
107
|
+
# IPv4-mapped IPv6 address (`::ffff:10.0.0.5`, what a dual-stack
|
|
108
|
+
# server reports for an IPv4 client) as the IPv4 address it holds.
|
|
109
|
+
#
|
|
110
|
+
# @param value [String, IPAddr, nil]
|
|
111
|
+
# @return [IPAddr, nil] nil for anything that is not an address
|
|
112
|
+
def request_address(value)
|
|
113
|
+
return if value.blank?
|
|
114
|
+
|
|
115
|
+
address = value.is_a?(IPAddr) ? value : IPAddr.new(value.to_s)
|
|
116
|
+
address.ipv4_mapped? ? address.native : address
|
|
117
|
+
rescue IPAddr::Error
|
|
118
|
+
nil
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# @param entry [String]
|
|
122
|
+
# @return [IPAddr, nil] nil for an entry that is not an IP address or a CIDR range
|
|
123
|
+
def ip_range(entry)
|
|
124
|
+
IPAddr.new(entry)
|
|
125
|
+
rescue IPAddr::Error
|
|
126
|
+
nil
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# @return [Array<String>] the entries of `allowed_ips` as written (.entries)
|
|
131
|
+
def allowed_ip_list
|
|
132
|
+
ActivationConditions.entries(allowed_ips)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# @return [Array<IPAddr>] the ranges of the entries that are addresses
|
|
136
|
+
def allowed_ip_ranges
|
|
137
|
+
allowed_ip_list.filter_map { |entry| ActivationConditions.ip_range(entry) }
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# @return [Array<String>] the entries of `allowed_ips` that are no address
|
|
141
|
+
def invalid_ip_entries
|
|
142
|
+
allowed_ip_list.reject { |entry| ActivationConditions.ip_range(entry) }
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Whether the IP list cannot be judged: text that holds no entry, or an
|
|
146
|
+
# entry that is not an address. A blank list is no condition.
|
|
147
|
+
#
|
|
148
|
+
# @return [Boolean]
|
|
149
|
+
def ip_list_broken?
|
|
150
|
+
return false if allowed_ips.blank?
|
|
151
|
+
|
|
152
|
+
allowed_ip_list.empty? || invalid_ip_entries.any?
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# @return [HoursWindow]
|
|
156
|
+
def hours_window
|
|
157
|
+
HoursWindow.new(from: allowed_hours_start, to: allowed_hours_end)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Whether a condition of the row cannot be judged (see the module).
|
|
161
|
+
#
|
|
162
|
+
# @return [Boolean]
|
|
163
|
+
def conditions_broken?
|
|
164
|
+
ip_list_broken? || hours_window.broken? || session_flag_broken?
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# A session flag no request can carry: its name holds a control
|
|
168
|
+
# character (a NUL byte, a newline, a tab). The validation refuses one.
|
|
169
|
+
#
|
|
170
|
+
# @return [Boolean]
|
|
171
|
+
def session_flag_broken?
|
|
172
|
+
requires_session_flag.to_s.match?(/[[:cntrl:]]/)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# A granting set whose condition nobody can judge: it never applies, with
|
|
176
|
+
# a request context or without one (ConditionalSets, Admin::EffectiveAccess).
|
|
177
|
+
# For an includer that answers `muting?` (PermissionSet, ConditionalSets::Member).
|
|
178
|
+
#
|
|
179
|
+
# @return [Boolean]
|
|
180
|
+
def never_grants?
|
|
181
|
+
!muting? && conditions_broken?
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# What the validations refuse about the row's conditions, for a stored
|
|
185
|
+
# row too: `[:ip_entry, entry]` per entry that is no address, `[:ip_empty,
|
|
186
|
+
# text]` for a list of separators, `[:flag_unusable, flag]` for a session
|
|
187
|
+
# flag no request can carry, and the window's problem with the window
|
|
188
|
+
# (:hours_incomplete, :hours_range, :hours_equal).
|
|
189
|
+
#
|
|
190
|
+
# @return [Array<Array(Symbol, Object)>]
|
|
191
|
+
def condition_problems
|
|
192
|
+
problems = invalid_ip_entries.map { |entry| [:ip_entry, entry] }
|
|
193
|
+
problems << [:flag_unusable, requires_session_flag] if session_flag_broken?
|
|
194
|
+
problems << [:ip_empty, allowed_ips] if allowed_ips.present? && allowed_ip_list.empty?
|
|
195
|
+
window = hours_window
|
|
196
|
+
problems << [window.problem, window] if window.problem
|
|
197
|
+
problems
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
# The one definition of the label the admin shows for an action: the
|
|
6
|
+
# label an admin wrote on the ResourceAction row — one the gem generated
|
|
7
|
+
# (PermissionForge::GeneratedText.label?: blank, the key, the key
|
|
8
|
+
# humanized) is none — else, on an English page, Rails' English for the
|
|
9
|
+
# key ("Export csv", what an English host saw before 0.15:
|
|
10
|
+
# FieldLabel.english), else the key verbatim ("export_csv"; `verbatim?`:
|
|
11
|
+
# the view draws it in <code>). An action with a row and one without
|
|
12
|
+
# read the same.
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# ActionLabel.for(action).text # => "Export to CSV" (the admin's label); "export_csv" on sq, "Export csv" on en
|
|
16
|
+
# ActionLabel.for(nil, 'sync').verbatim? # => true on a page that is not English
|
|
17
|
+
#
|
|
18
|
+
# @api private
|
|
19
|
+
module ActionLabel
|
|
20
|
+
# @param action [ResourceAction, #label, nil] the row, when there is one
|
|
21
|
+
# @param key [String, Symbol] the action key (the row's name by default)
|
|
22
|
+
# @return [FieldLabel::Label]
|
|
23
|
+
def self.for(action, key = action&.name)
|
|
24
|
+
key = key.to_s
|
|
25
|
+
label = FieldLabel.written_label(action, key)
|
|
26
|
+
return FieldLabel::Label.new(text: label, verbatim: false) if label
|
|
27
|
+
|
|
28
|
+
english = FieldLabel.english(key)
|
|
29
|
+
english ? FieldLabel::Label.new(text: english, verbatim: false) : FieldLabel::Label.new(text: key, verbatim: true)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -24,6 +24,8 @@ module PermissionForge
|
|
|
24
24
|
delegate :effect, :permitted_by, to: :standing
|
|
25
25
|
|
|
26
26
|
def configured? = !action.nil?
|
|
27
|
+
# The label is the key itself (ActionLabel): drawn once, in <code>.
|
|
28
|
+
def verbatim? = label == name
|
|
27
29
|
end
|
|
28
30
|
|
|
29
31
|
# What one action means right now for people with this set, and which
|
|
@@ -147,7 +149,7 @@ module PermissionForge
|
|
|
147
149
|
|
|
148
150
|
def build_row(key, action)
|
|
149
151
|
endpoints = endpoints_by_key.fetch(key, [])
|
|
150
|
-
Row.new(name: key, label: action
|
|
152
|
+
Row.new(name: key, label: ActionLabel.for(action, key).text, action: action, endpoints: endpoints,
|
|
151
153
|
unlinked: discovery? && endpoints.empty?,
|
|
152
154
|
standing: Standing.new(key, action, across: @across, configuration: @configuration))
|
|
153
155
|
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
# How the admin names who made a change — the one definition the record
|
|
6
|
+
# footers, the event page, the audit tabs and their CSV, "Assigned by" and
|
|
7
|
+
# the event sentences use: a label the gem stored as a code
|
|
8
|
+
# (Audit::ActorCodes: an API client nobody named, the boot) in the
|
|
9
|
+
# page's language, else the event's actor label (with its kind where
|
|
10
|
+
# it is not a person: "admin (password prompt)"), else "Person #<id>",
|
|
11
|
+
# else the actor kind's word (StateLabel kind :actor: "System",
|
|
12
|
+
# "Unknown"). A record with no event and no created_by / updated_by is
|
|
13
|
+
# "System" (#system).
|
|
14
|
+
#
|
|
15
|
+
# @api private
|
|
16
|
+
module ActorLabel
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
# @param event [PermissionForge::AuditEvent]
|
|
20
|
+
# @return [String]
|
|
21
|
+
def for(event)
|
|
22
|
+
type = event.actor_type.to_s
|
|
23
|
+
label = event.actor_label.presence
|
|
24
|
+
code = PermissionForge::Audit::ActorCodes.code(type, label)
|
|
25
|
+
return I18n.t("permission_forge.audit_log.actor_codes.#{code}") if code
|
|
26
|
+
return I18n.t("permission_forge.audit_log.actor_named.#{type}", name: label, default: label) if label
|
|
27
|
+
return I18n.t('permission_forge.audit_log.person_id', id: event.actor_id) if type == 'user' && event.actor_id.present?
|
|
28
|
+
|
|
29
|
+
type_label(type)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# The word of an actor kind; a kind outside the catalogue as stored.
|
|
33
|
+
#
|
|
34
|
+
# @param type [String, Symbol]
|
|
35
|
+
# @return [String]
|
|
36
|
+
def type_label(type) = StateLabel.word(:actor, type, default: type.to_s)
|
|
37
|
+
|
|
38
|
+
# Whether the event names nobody: its actor kind is unknown and it has no label.
|
|
39
|
+
#
|
|
40
|
+
# @param event [PermissionForge::AuditEvent, nil]
|
|
41
|
+
# @return [Boolean]
|
|
42
|
+
def unknown?(event)
|
|
43
|
+
!event.nil? && event.actor_type.to_s == 'unknown' && event.actor_label.blank?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Who a record with no event and no person stamped on it was changed by.
|
|
47
|
+
#
|
|
48
|
+
# @return [String]
|
|
49
|
+
def system = StateLabel.word(:actor, :system)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|