permission_forge 0.14.1
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 +7 -0
- data/CHANGELOG.md +1138 -0
- data/LICENSE.txt +21 -0
- data/README.md +356 -0
- data/app/controllers/concerns/permission_forge/acting_admin.rb +17 -0
- data/app/controllers/concerns/permission_forge/api/crud_actions.rb +84 -0
- data/app/controllers/concerns/permission_forge/api/nested_resource_lookup.rb +18 -0
- data/app/controllers/concerns/permission_forge/api/pagination.rb +50 -0
- data/app/controllers/concerns/permission_forge/api/response_renderer.rb +65 -0
- data/app/controllers/concerns/permission_forge/api/serializer_resolver.rb +30 -0
- data/app/controllers/concerns/permission_forge/api_authentication.rb +40 -0
- data/app/controllers/concerns/permission_forge/assignment_authorization.rb +97 -0
- data/app/controllers/concerns/permission_forge/audit_log_gate.rb +55 -0
- data/app/controllers/concerns/permission_forge/basic_authentication.rb +77 -0
- data/app/controllers/concerns/permission_forge/cache_confirmation.rb +32 -0
- data/app/controllers/concerns/permission_forge/forgery_protection.rb +29 -0
- data/app/controllers/concerns/permission_forge/html_format.rb +40 -0
- data/app/controllers/concerns/permission_forge/param_shapes.rb +87 -0
- data/app/controllers/concerns/permission_forge/tenant_required.rb +42 -0
- data/app/controllers/permission_forge/api/audit_events_controller.rb +33 -0
- data/app/controllers/permission_forge/api/base_controller.rb +220 -0
- data/app/controllers/permission_forge/api/dashboard_controller.rb +30 -0
- data/app/controllers/permission_forge/api/permission_set_group_users_controller.rb +51 -0
- data/app/controllers/permission_forge/api/permission_set_groups_controller.rb +71 -0
- data/app/controllers/permission_forge/api/permission_set_rules_controller.rb +37 -0
- data/app/controllers/permission_forge/api/permission_set_users_controller.rb +105 -0
- data/app/controllers/permission_forge/api/permission_sets_controller.rb +87 -0
- data/app/controllers/permission_forge/api/resource_actions_controller.rb +58 -0
- data/app/controllers/permission_forge/api/resource_fields_controller.rb +30 -0
- data/app/controllers/permission_forge/api/resources_controller.rb +56 -0
- data/app/controllers/permission_forge/application_controller.rb +198 -0
- data/app/controllers/permission_forge/assets_controller.rb +97 -0
- data/app/controllers/permission_forge/assignments_controller.rb +266 -0
- data/app/controllers/permission_forge/audit_events_controller.rb +94 -0
- data/app/controllers/permission_forge/bulk_assignments_controller.rb +108 -0
- data/app/controllers/permission_forge/field_lookups_controller.rb +38 -0
- data/app/controllers/permission_forge/home_controller.rb +24 -0
- data/app/controllers/permission_forge/maintenance_controller.rb +13 -0
- data/app/controllers/permission_forge/people_controller.rb +190 -0
- data/app/controllers/permission_forge/permission_set_editor.rb +71 -0
- data/app/controllers/permission_forge/permission_set_group_users_controller.rb +57 -0
- data/app/controllers/permission_forge/permission_set_groups_controller.rb +111 -0
- data/app/controllers/permission_forge/permission_set_rules_controller.rb +97 -0
- data/app/controllers/permission_forge/permission_sets_controller.rb +227 -0
- data/app/controllers/permission_forge/preferences_controller.rb +26 -0
- data/app/controllers/permission_forge/resource_actions_controller.rb +106 -0
- data/app/controllers/permission_forge/resource_fields_controller.rb +129 -0
- data/app/controllers/permission_forge/resources_controller.rb +189 -0
- data/app/controllers/permission_forge/settings_controller.rb +33 -0
- data/app/frontend/permission_forge/brand/apple-touch-icon.png +0 -0
- data/app/frontend/permission_forge/brand/chevron.svg +1 -0
- data/app/frontend/permission_forge/brand/favicon.ico +0 -0
- data/app/frontend/permission_forge/brand/favicon.svg +1 -0
- data/app/frontend/permission_forge/brand/logo-horizontal-dark.svg +1 -0
- data/app/frontend/permission_forge/brand/logo-horizontal-light.svg +1 -0
- data/app/frontend/permission_forge/brand/mark-small.svg +1 -0
- data/app/frontend/permission_forge/brand/mark.svg +1 -0
- data/app/frontend/permission_forge/pf_admin.css +766 -0
- data/app/frontend/permission_forge/pf_admin.js +482 -0
- data/app/helpers/permission_forge/application_helper.rb +167 -0
- data/app/helpers/permission_forge/audit_helper.rb +230 -0
- data/app/helpers/permission_forge/audit_sentences_helper.rb +84 -0
- data/app/helpers/permission_forge/badges_helper.rb +134 -0
- data/app/helpers/permission_forge/changes_helper.rb +117 -0
- data/app/helpers/permission_forge/people_helper.rb +118 -0
- data/app/helpers/permission_forge/sentences_helper.rb +175 -0
- data/app/helpers/permission_forge/settings_helper.rb +152 -0
- data/app/helpers/permission_forge/ui_helper.rb +184 -0
- data/app/models/permission_forge/audit_event.rb +159 -0
- data/app/models/permission_forge/permission_set.rb +100 -0
- data/app/models/permission_forge/permission_set_group.rb +83 -0
- data/app/models/permission_forge/permission_set_group_user.rb +43 -0
- data/app/models/permission_forge/permission_set_rule.rb +99 -0
- data/app/models/permission_forge/permission_set_user.rb +158 -0
- data/app/models/permission_forge/resource.rb +100 -0
- data/app/models/permission_forge/resource_action.rb +58 -0
- data/app/models/permission_forge/resource_field.rb +69 -0
- data/app/views/layouts/permission_forge/application.html.erb +30 -0
- data/app/views/layouts/permission_forge/fragment.html.erb +2 -0
- data/app/views/permission_forge/assignments/_person_options.html.erb +26 -0
- data/app/views/permission_forge/assignments/confirm_destroy.html.erb +24 -0
- data/app/views/permission_forge/assignments/new.html.erb +99 -0
- data/app/views/permission_forge/audit_events/index.html.erb +161 -0
- data/app/views/permission_forge/audit_events/show.html.erb +146 -0
- data/app/views/permission_forge/bulk_assignments/confirm_revoke.html.erb +21 -0
- data/app/views/permission_forge/bulk_assignments/new.html.erb +69 -0
- data/app/views/permission_forge/field_lookups/show.html.erb +32 -0
- data/app/views/permission_forge/home/_audit_card.html.erb +19 -0
- data/app/views/permission_forge/home/_ending_card.html.erb +32 -0
- data/app/views/permission_forge/home/_expired_card.html.erb +34 -0
- data/app/views/permission_forge/home/_privacy_card.html.erb +67 -0
- data/app/views/permission_forge/home/_rules_card.html.erb +23 -0
- data/app/views/permission_forge/home/_stale_card.html.erb +39 -0
- data/app/views/permission_forge/home/_tidy_card.html.erb +18 -0
- data/app/views/permission_forge/home/index.html.erb +69 -0
- data/app/views/permission_forge/people/_access_tab.html.erb +58 -0
- data/app/views/permission_forge/people/_audit_tab.html.erb +93 -0
- data/app/views/permission_forge/people/_effective_field_source.html.erb +11 -0
- data/app/views/permission_forge/people/_effective_tab.html.erb +118 -0
- data/app/views/permission_forge/people/_effective_why.html.erb +48 -0
- data/app/views/permission_forge/people/_why_item.html.erb +19 -0
- data/app/views/permission_forge/people/confirm_remove_all.html.erb +27 -0
- data/app/views/permission_forge/people/consent_preview.html.erb +39 -0
- data/app/views/permission_forge/people/erasure_preview.html.erb +28 -0
- data/app/views/permission_forge/people/index.html.erb +78 -0
- data/app/views/permission_forge/people/show.html.erb +54 -0
- data/app/views/permission_forge/permission_set_groups/confirm_destroy.html.erb +12 -0
- data/app/views/permission_forge/permission_set_groups/index.html.erb +55 -0
- data/app/views/permission_forge/permission_set_groups/show.html.erb +100 -0
- data/app/views/permission_forge/permission_set_rules/_builder.html.erb +145 -0
- data/app/views/permission_forge/permission_set_rules/_value_input.html.erb +42 -0
- data/app/views/permission_forge/permission_set_rules/confirm_destroy.html.erb +13 -0
- data/app/views/permission_forge/permission_set_rules/edit.html.erb +3 -0
- data/app/views/permission_forge/permission_set_rules/new.html.erb +3 -0
- data/app/views/permission_forge/permission_sets/_assignments_tab.html.erb +74 -0
- data/app/views/permission_forge/permission_sets/_audit_tab.html.erb +80 -0
- data/app/views/permission_forge/permission_sets/_conditions_tab.html.erb +83 -0
- data/app/views/permission_forge/permission_sets/_header.html.erb +36 -0
- data/app/views/permission_forge/permission_sets/_records_summary.html.erb +16 -0
- data/app/views/permission_forge/permission_sets/_resource_summary.html.erb +11 -0
- data/app/views/permission_forge/permission_sets/_resources_tab.html.erb +49 -0
- data/app/views/permission_forge/permission_sets/confirm_destroy.html.erb +23 -0
- data/app/views/permission_forge/permission_sets/confirm_switch_mode.html.erb +15 -0
- data/app/views/permission_forge/permission_sets/index.html.erb +83 -0
- data/app/views/permission_forge/permission_sets/new.html.erb +63 -0
- data/app/views/permission_forge/permission_sets/review.html.erb +26 -0
- data/app/views/permission_forge/permission_sets/show.html.erb +50 -0
- data/app/views/permission_forge/resource_actions/_across_sets.html.erb +24 -0
- data/app/views/permission_forge/resource_actions/_add_select.html.erb +12 -0
- data/app/views/permission_forge/resource_actions/_effect.html.erb +25 -0
- data/app/views/permission_forge/resource_actions/_endpoints.html.erb +16 -0
- data/app/views/permission_forge/resource_actions/_form.html.erb +44 -0
- data/app/views/permission_forge/resource_actions/_managed_note.html.erb +11 -0
- data/app/views/permission_forge/resource_actions/confirm_destroy.html.erb +13 -0
- data/app/views/permission_forge/resource_actions/new.html.erb +31 -0
- data/app/views/permission_forge/resource_actions/show.html.erb +39 -0
- data/app/views/permission_forge/resource_fields/_explain.html.erb +24 -0
- data/app/views/permission_forge/resource_fields/show.html.erb +186 -0
- data/app/views/permission_forge/resources/_actions_tab.html.erb +88 -0
- data/app/views/permission_forge/resources/_fields_tab.html.erb +89 -0
- data/app/views/permission_forge/resources/_rules_tab.html.erb +55 -0
- data/app/views/permission_forge/resources/_sync.html.erb +57 -0
- data/app/views/permission_forge/resources/confirm_destroy.html.erb +17 -0
- data/app/views/permission_forge/resources/new.html.erb +57 -0
- data/app/views/permission_forge/resources/show.html.erb +117 -0
- data/app/views/permission_forge/settings/_ladder.html.erb +15 -0
- data/app/views/permission_forge/settings/_row.html.erb +38 -0
- data/app/views/permission_forge/settings/show.html.erb +67 -0
- data/app/views/permission_forge/shared/_across_sets.html.erb +31 -0
- data/app/views/permission_forge/shared/_audit_events.html.erb +36 -0
- data/app/views/permission_forge/shared/_audit_footer.html.erb +16 -0
- data/app/views/permission_forge/shared/_audit_off_note.html.erb +15 -0
- data/app/views/permission_forge/shared/_audit_pagination.html.erb +19 -0
- data/app/views/permission_forge/shared/_audit_refused_note.html.erb +7 -0
- data/app/views/permission_forge/shared/_audit_timeline.html.erb +29 -0
- data/app/views/permission_forge/shared/_crud_cell.html.erb +18 -0
- data/app/views/permission_forge/shared/_dialog_frame.html.erb +11 -0
- data/app/views/permission_forge/shared/_expiry_picker.html.erb +27 -0
- data/app/views/permission_forge/shared/_head_extra.html.erb +3 -0
- data/app/views/permission_forge/shared/_help_banner.html.erb +10 -0
- data/app/views/permission_forge/shared/_level_control.html.erb +18 -0
- data/app/views/permission_forge/shared/_mode_banners.html.erb +22 -0
- data/app/views/permission_forge/shared/_nav.html.erb +12 -0
- data/app/views/permission_forge/shared/_preferences.html.erb +24 -0
- data/app/views/permission_forge/shared/_reason_field.html.erb +20 -0
- data/app/views/permission_forge/shared/_savebar.html.erb +13 -0
- data/app/views/permission_forge/shared/_sheet_frame.html.erb +22 -0
- data/app/views/permission_forge/shared/_sidebar.html.erb +18 -0
- data/app/views/permission_forge/shared/_tech_details.html.erb +20 -0
- data/app/views/permission_forge/shared/_toasts.html.erb +27 -0
- data/app/views/permission_forge/shared/_topbar.html.erb +15 -0
- data/app/views/permission_forge/shared/_user_select.html.erb +16 -0
- data/app/views/permission_forge/shared/audit_refused.html.erb +7 -0
- data/config/locales/permission_forge.en.yml +2115 -0
- data/config/locales/permission_forge.mk.yml +2166 -0
- data/config/locales/permission_forge.sq.yml +2164 -0
- data/config/routes.rb +143 -0
- data/lib/generators/permission_forge/install/install_generator.rb +105 -0
- data/lib/generators/permission_forge/install/templates/initializer.rb.tt +148 -0
- data/lib/generators/permission_forge/install/templates/migration.rb.tt +198 -0
- data/lib/generators/permission_forge/rls/rls_generator.rb +57 -0
- data/lib/generators/permission_forge/rls/templates/enable_permission_forge_rls.rb.tt +15 -0
- data/lib/generators/permission_forge/upgrade/templates/add_admin_ui_columns.rb.tt +8 -0
- data/lib/generators/permission_forge/upgrade/templates/add_audit_events.rb.tt +98 -0
- data/lib/generators/permission_forge/upgrade/templates/add_resource_fields_unique_index.rb.tt +69 -0
- data/lib/generators/permission_forge/upgrade/upgrade_generator.rb +66 -0
- data/lib/permission_forge/admin/action_across_sets.rb +76 -0
- data/lib/permission_forge/admin/action_rows.rb +156 -0
- data/lib/permission_forge/admin/attention_items.rb +207 -0
- data/lib/permission_forge/admin/audit_event_detail.rb +75 -0
- data/lib/permission_forge/admin/audit_log.rb +193 -0
- data/lib/permission_forge/admin/audit_log_days.rb +41 -0
- data/lib/permission_forge/admin/audit_lookups.rb +105 -0
- data/lib/permission_forge/admin/audit_name_history.rb +85 -0
- data/lib/permission_forge/admin/audit_trail.rb +169 -0
- data/lib/permission_forge/admin/bulk_people_matcher.rb +109 -0
- data/lib/permission_forge/admin/change_set.rb +123 -0
- data/lib/permission_forge/admin/change_set_parts/actions.rb +27 -0
- data/lib/permission_forge/admin/change_set_parts/base.rb +58 -0
- data/lib/permission_forge/admin/change_set_parts/context.rb +50 -0
- data/lib/permission_forge/admin/change_set_parts/field_edit.rb +67 -0
- data/lib/permission_forge/admin/change_set_parts/field_levels.rb +71 -0
- data/lib/permission_forge/admin/change_set_parts/new_actions.rb +91 -0
- data/lib/permission_forge/admin/change_set_parts/new_fields.rb +63 -0
- data/lib/permission_forge/admin/change_set_parts/resource_edit.rb +55 -0
- data/lib/permission_forge/admin/change_set_parts/resource_flags.rb +19 -0
- data/lib/permission_forge/admin/change_set_parts/set_attrs.rb +45 -0
- data/lib/permission_forge/admin/change_set_parts/values.rb +20 -0
- data/lib/permission_forge/admin/column_fields.rb +48 -0
- data/lib/permission_forge/admin/conditions_form.rb +78 -0
- data/lib/permission_forge/admin/controller_catalog.rb +166 -0
- data/lib/permission_forge/admin/csv.rb +41 -0
- data/lib/permission_forge/admin/editable_lock.rb +157 -0
- data/lib/permission_forge/admin/effective_access.rb +158 -0
- data/lib/permission_forge/admin/effective_fields.rb +176 -0
- data/lib/permission_forge/admin/expiry_presets.rb +51 -0
- data/lib/permission_forge/admin/exports/document.rb +89 -0
- data/lib/permission_forge/admin/exports/events.rb +51 -0
- data/lib/permission_forge/admin/exports/holdings.rb +118 -0
- data/lib/permission_forge/admin/exports.rb +37 -0
- data/lib/permission_forge/admin/field_across_sets.rb +51 -0
- data/lib/permission_forge/admin/field_detail.rb +112 -0
- data/lib/permission_forge/admin/field_level.rb +74 -0
- data/lib/permission_forge/admin/field_rows.rb +137 -0
- data/lib/permission_forge/admin/last_changes.rb +94 -0
- data/lib/permission_forge/admin/model_catalog.rb +216 -0
- data/lib/permission_forge/admin/people_candidates.rb +108 -0
- data/lib/permission_forge/admin/people_directory.rb +158 -0
- data/lib/permission_forge/admin/permission_set_factory.rb +48 -0
- data/lib/permission_forge/admin/person_label.rb +95 -0
- data/lib/permission_forge/admin/record_id.rb +19 -0
- data/lib/permission_forge/admin/rule_builder.rb +58 -0
- data/lib/permission_forge/admin/rule_form.rb +175 -0
- data/lib/permission_forge/admin/rule_form_input.rb +74 -0
- data/lib/permission_forge/admin/sensitive_columns.rb +68 -0
- data/lib/permission_forge/admin/settings_catalog.rb +261 -0
- data/lib/permission_forge/admin/settings_values.rb +130 -0
- data/lib/permission_forge/api/serializers/audit_event_serializer.rb +27 -0
- data/lib/permission_forge/api/serializers/base_serializer.rb +36 -0
- data/lib/permission_forge/api/serializers/permission_set_group_serializer.rb +12 -0
- data/lib/permission_forge/api/serializers/permission_set_group_user_serializer.rb +11 -0
- data/lib/permission_forge/api/serializers/permission_set_rule_serializer.rb +25 -0
- data/lib/permission_forge/api/serializers/permission_set_serializer.rb +13 -0
- data/lib/permission_forge/api/serializers/permission_set_user_serializer.rb +11 -0
- data/lib/permission_forge/api/serializers/resource_action_serializer.rb +13 -0
- data/lib/permission_forge/api/serializers/resource_field_serializer.rb +14 -0
- data/lib/permission_forge/api/serializers/resource_serializer.rb +15 -0
- data/lib/permission_forge/audit/actor.rb +144 -0
- data/lib/permission_forge/audit/actor_resolver.rb +103 -0
- data/lib/permission_forge/audit/audited_views.rb +208 -0
- data/lib/permission_forge/audit/boot_warnings.rb +58 -0
- data/lib/permission_forge/audit/column_values.rb +56 -0
- data/lib/permission_forge/audit/context.rb +98 -0
- data/lib/permission_forge/audit/detached.rb +62 -0
- data/lib/permission_forge/audit/event_catalog.rb +110 -0
- data/lib/permission_forge/audit/event_query.rb +192 -0
- data/lib/permission_forge/audit/failure.rb +47 -0
- data/lib/permission_forge/audit/field_access.rb +186 -0
- data/lib/permission_forge/audit/gate.rb +167 -0
- data/lib/permission_forge/audit/job_context.rb +32 -0
- data/lib/permission_forge/audit/log_access.rb +36 -0
- data/lib/permission_forge/audit/recorder.rb +168 -0
- data/lib/permission_forge/audit/refusals.rb +61 -0
- data/lib/permission_forge/audit/request_context.rb +152 -0
- data/lib/permission_forge/audit/runtime.rb +178 -0
- data/lib/permission_forge/audit/runtime_emission.rb +135 -0
- data/lib/permission_forge/audit/settings_observer.rb +70 -0
- data/lib/permission_forge/audit/source.rb +54 -0
- data/lib/permission_forge/audit/subscribers/base.rb +69 -0
- data/lib/permission_forge/audit/subscribers/legacy_hooks.rb +97 -0
- data/lib/permission_forge/audit/subscribers/log.rb +45 -0
- data/lib/permission_forge/audit/subscribers.rb +58 -0
- data/lib/permission_forge/audit.rb +302 -0
- data/lib/permission_forge/boot_checks.rb +184 -0
- data/lib/permission_forge/cache/backlog.rb +142 -0
- data/lib/permission_forge/cache/base.rb +200 -0
- data/lib/permission_forge/cache/bulk_invalidation.rb +131 -0
- data/lib/permission_forge/cache/failure.rb +171 -0
- data/lib/permission_forge/cache/handled_errors.rb +82 -0
- data/lib/permission_forge/cache/holder_invalidation.rb +85 -0
- data/lib/permission_forge/cache/invalidation.rb +180 -0
- data/lib/permission_forge/cache/memory_cache.rb +77 -0
- data/lib/permission_forge/cache/null_cache.rb +40 -0
- data/lib/permission_forge/cache/rails_cache.rb +231 -0
- data/lib/permission_forge/cache/rebuild.rb +149 -0
- data/lib/permission_forge/cache/settlement.rb +63 -0
- data/lib/permission_forge/cache/user_keys.rb +206 -0
- data/lib/permission_forge/cache/variant_removal.rb +55 -0
- data/lib/permission_forge/cache/withheld_notice.rb +60 -0
- data/lib/permission_forge/concerns/audited_collection.rb +34 -0
- data/lib/permission_forge/concerns/auto_permission_scoping.rb +158 -0
- data/lib/permission_forge/concerns/controller_action_guard.rb +106 -0
- data/lib/permission_forge/concerns/field_mask_checker.rb +45 -0
- data/lib/permission_forge/concerns/has_active_scope.rb +12 -0
- data/lib/permission_forge/concerns/has_audit_trail.rb +59 -0
- data/lib/permission_forge/concerns/has_permission_set_users.rb +115 -0
- data/lib/permission_forge/concerns/has_tenant_scoping.rb +86 -0
- data/lib/permission_forge/concerns/has_uuid.rb +24 -0
- data/lib/permission_forge/concerns/invalidates_in_batch.rb +207 -0
- data/lib/permission_forge/concerns/invalidates_managed_actions.rb +80 -0
- data/lib/permission_forge/concerns/masked_attributes/alias_access.rb +280 -0
- data/lib/permission_forge/concerns/masked_attributes/store_access.rb +100 -0
- data/lib/permission_forge/concerns/masked_attributes/validation_reads.rb +153 -0
- data/lib/permission_forge/concerns/masked_attributes/virtual_readers.rb +242 -0
- data/lib/permission_forge/concerns/masked_attributes.rb +576 -0
- data/lib/permission_forge/concerns/permission_cache_accessor.rb +416 -0
- data/lib/permission_forge/concerns/permission_scoped_findable.rb +337 -0
- data/lib/permission_forge/concerns/permissioned_user.rb +78 -0
- data/lib/permission_forge/concerns/permissions_memo.rb +160 -0
- data/lib/permission_forge/concerns/permissions_per_context.rb +288 -0
- data/lib/permission_forge/concerns/permissions_regenerator.rb +43 -0
- data/lib/permission_forge/concerns/records_audit_events.rb +286 -0
- data/lib/permission_forge/concerns/resource_permission_checker.rb +116 -0
- data/lib/permission_forge/concerns/validates_column_lengths.rb +69 -0
- data/lib/permission_forge/concerns/validates_dependency.rb +54 -0
- data/lib/permission_forge/conditional_sets.rb +361 -0
- data/lib/permission_forge/configuration/admin_ui_settings.rb +108 -0
- data/lib/permission_forge/configuration/audit_settings.rb +184 -0
- data/lib/permission_forge/configuration/cache_settings.rb +76 -0
- data/lib/permission_forge/configuration/controller_action_settings.rb +112 -0
- data/lib/permission_forge/configuration/gdpr_settings.rb +70 -0
- data/lib/permission_forge/configuration/masked_attributes_settings.rb +83 -0
- data/lib/permission_forge/configuration/permits_attributes_settings.rb +26 -0
- data/lib/permission_forge/configuration/presets.rb +139 -0
- data/lib/permission_forge/configuration/query_scoping_settings.rb +107 -0
- data/lib/permission_forge/configuration/strategy_values.rb +57 -0
- data/lib/permission_forge/configuration/virtual_attributes_settings.rb +65 -0
- data/lib/permission_forge/configuration.rb +624 -0
- data/lib/permission_forge/controller_actions/authorizer.rb +171 -0
- data/lib/permission_forge/controller_actions/managed_index.rb +213 -0
- data/lib/permission_forge/controller_actions/mapping.rb +167 -0
- data/lib/permission_forge/controller_actions.rb +122 -0
- data/lib/permission_forge/deprecation.rb +86 -0
- data/lib/permission_forge/engine.rb +113 -0
- data/lib/permission_forge/execution_state.rb +318 -0
- data/lib/permission_forge/invalidation_batch.rb +206 -0
- data/lib/permission_forge/masked_models.rb +226 -0
- data/lib/permission_forge/masking_sync.rb +271 -0
- data/lib/permission_forge/model_bindings.rb +90 -0
- data/lib/permission_forge/open_transaction.rb +303 -0
- data/lib/permission_forge/operators/constants.rb +41 -0
- data/lib/permission_forge/operators/registry.rb +300 -0
- data/lib/permission_forge/operators/rule_column.rb +120 -0
- data/lib/permission_forge/operators/validations.rb +111 -0
- data/lib/permission_forge/operators/value_casters/number.rb +68 -0
- data/lib/permission_forge/operators/value_casters/temporal.rb +69 -0
- data/lib/permission_forge/operators/value_casters.rb +80 -0
- data/lib/permission_forge/operators/value_typing.rb +115 -0
- data/lib/permission_forge/query_scoping_warnings.rb +68 -0
- data/lib/permission_forge/rake_audit.rb +65 -0
- data/lib/permission_forge/resource_chain.rb +190 -0
- data/lib/permission_forge/rls.rb +209 -0
- data/lib/permission_forge/services/attribute_access_guard.rb +138 -0
- data/lib/permission_forge/services/audit_event_purger.rb +150 -0
- data/lib/permission_forge/services/bulk_permission_set_assignment.rb +108 -0
- data/lib/permission_forge/services/concerns/across_tenants.rb +69 -0
- data/lib/permission_forge/services/concerns/audited_operation.rb +43 -0
- data/lib/permission_forge/services/concerns/clonable.rb +29 -0
- data/lib/permission_forge/services/consent_withdrawal_handler.rb +135 -0
- data/lib/permission_forge/services/dashboard_stats.rb +43 -0
- data/lib/permission_forge/services/data_subject_erasure.rb +306 -0
- data/lib/permission_forge/services/expired_assignment_cleaner.rb +152 -0
- data/lib/permission_forge/services/extra_field_defaults.rb +71 -0
- data/lib/permission_forge/services/field_permission_merger.rb +83 -0
- data/lib/permission_forge/services/filters_response_attributes.rb +144 -0
- data/lib/permission_forge/services/gdpr_field_auditor/text_report.rb +63 -0
- data/lib/permission_forge/services/gdpr_field_auditor.rb +223 -0
- data/lib/permission_forge/services/host_key_guard.rb +110 -0
- data/lib/permission_forge/services/mask_value.rb +93 -0
- data/lib/permission_forge/services/permission_cache_clearer.rb +174 -0
- data/lib/permission_forge/services/permission_hash_builder.rb +305 -0
- data/lib/permission_forge/services/permission_rule_applier.rb +192 -0
- data/lib/permission_forge/services/permission_set_cloner.rb +89 -0
- data/lib/permission_forge/services/permission_set_condition_evaluator.rb +102 -0
- data/lib/permission_forge/services/permission_set_group_cloner.rb +52 -0
- data/lib/permission_forge/services/permits_attributes/nested_records.rb +106 -0
- data/lib/permission_forge/services/permits_attributes.rb +270 -0
- data/lib/permission_forge/services/resource_field_generator.rb +93 -0
- data/lib/permission_forge/services/resource_with_fields_builder.rb +47 -0
- data/lib/permission_forge/services/rls_operator_sql.rb +131 -0
- data/lib/permission_forge/services/rls_policy_generator.rb +365 -0
- data/lib/permission_forge/services/security_audit/action_check.rb +157 -0
- data/lib/permission_forge/services/security_audit/assignment_check.rb +120 -0
- data/lib/permission_forge/services/security_audit/audit_trail_check.rb +166 -0
- data/lib/permission_forge/services/security_audit/base_check.rb +57 -0
- data/lib/permission_forge/services/security_audit/config_check.rb +212 -0
- data/lib/permission_forge/services/security_audit/integrity_check.rb +105 -0
- data/lib/permission_forge/services/security_audit/personal_data_check.rb +79 -0
- data/lib/permission_forge/services/security_audit/privilege_check.rb +67 -0
- data/lib/permission_forge/services/security_audit/rule_check.rb +141 -0
- data/lib/permission_forge/services/security_audit/text_report.rb +46 -0
- data/lib/permission_forge/services/security_audit/user_model_check.rb +67 -0
- data/lib/permission_forge/services/security_audit.rb +75 -0
- data/lib/permission_forge/services/stale_assignment_auditor.rb +68 -0
- data/lib/permission_forge/services/unscoped_query_detector.rb +132 -0
- data/lib/permission_forge/version.rb +3 -0
- data/lib/permission_forge/virtual_fields/discovery.rb +65 -0
- data/lib/permission_forge/virtual_fields.rb +102 -0
- data/lib/permission_forge.rb +464 -0
- data/lib/tasks/permission_forge.rake +31 -0
- data/lib/tasks/permission_forge_audit_events.rake +28 -0
- data/lib/tasks/permission_forge_cache.rake +26 -0
- data/lib/tasks/permission_forge_expiration.rake +30 -0
- data/lib/tasks/permission_forge_gdpr.rake +69 -0
- data/lib/tasks/permission_forge_rls.rake +38 -0
- metadata +515 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Concerns
|
|
5
|
+
module MaskedAttributes
|
|
6
|
+
# `alias_attribute` on a model with MaskedAttributes. An alias is not a
|
|
7
|
+
# field of its own: reading or writing it is reading or writing the
|
|
8
|
+
# attribute it points to, at that attribute's level.
|
|
9
|
+
#
|
|
10
|
+
# Rails generates an alias's reader and writer to reach the attribute
|
|
11
|
+
# directly — always on Rails 7.2 and later, and on 7.1 unless the model
|
|
12
|
+
# defines the target's method itself — past the masked getter and the
|
|
13
|
+
# guarded setter. So every alias gets a reader and a writer here that do
|
|
14
|
+
# what the Rails version in use does, plus the guard (see
|
|
15
|
+
# {AttributeAccessors} and {TargetMethodAccessors}).
|
|
16
|
+
#
|
|
17
|
+
# Where they live decides which of the model's own methods still run.
|
|
18
|
+
# Rails keeps an alias's methods in the generated-attribute-methods
|
|
19
|
+
# module of the class that declares it; the accessors go in a module
|
|
20
|
+
# prepended to that very module. Whatever is found before Rails' own
|
|
21
|
+
# alias methods — the class's methods, modules it includes, subclasses —
|
|
22
|
+
# is found before these too, and `super` there reaches them.
|
|
23
|
+
#
|
|
24
|
+
# They never call `super` themselves: Ruby (3.4 at least) does not
|
|
25
|
+
# notice that the method behind a module prepended to an already
|
|
26
|
+
# included module was redefined, and would keep calling the old one.
|
|
27
|
+
#
|
|
28
|
+
# The other methods Rails generates for an alias (`mail?`, `mail_was`,
|
|
29
|
+
# `mail_changed?`, `mail_before_type_cast`…) are already the target's
|
|
30
|
+
# (`email?`, `email_was`…) under another name and are left alone.
|
|
31
|
+
#
|
|
32
|
+
# @api private
|
|
33
|
+
module AliasAccess
|
|
34
|
+
extend ActiveSupport::Concern
|
|
35
|
+
|
|
36
|
+
# The module a class's alias readers and writers live in.
|
|
37
|
+
class AccessorModule < Module; end
|
|
38
|
+
|
|
39
|
+
# `id_value`, which Rails itself aliases to `id`, stays Rails': `id`
|
|
40
|
+
# is the primary key reader, not the reader of a column called id.
|
|
41
|
+
UNMASKED_TARGET = 'id'
|
|
42
|
+
|
|
43
|
+
CREATION = Mutex.new
|
|
44
|
+
private_constant :CREATION
|
|
45
|
+
|
|
46
|
+
# Rails 7.2 and later: an alias reads and writes the attribute itself,
|
|
47
|
+
# whatever reader or setter the model defines for the target. So do
|
|
48
|
+
# these: through `read_attribute` when the target's reader is one the
|
|
49
|
+
# concern masks, and past the write-back guard when the target is a
|
|
50
|
+
# masked column.
|
|
51
|
+
#
|
|
52
|
+
# An alias declared by a class without the concern (a parent of the
|
|
53
|
+
# one that has it) is reached by records of its other subclasses too.
|
|
54
|
+
# For those, this is what Rails generates, word for word. Such a class
|
|
55
|
+
# may declare the alias again with another target without the concern
|
|
56
|
+
# hearing of it, so the target is asked of the declaring class at each
|
|
57
|
+
# call, as Rails generates the alias's methods again there.
|
|
58
|
+
module AttributeAccessors
|
|
59
|
+
def self.define(klass, accessors, name, declared)
|
|
60
|
+
accessors.redefine_method(name, &reader(klass, name, declared))
|
|
61
|
+
accessors.redefine_method(:"#{name}=", &writer(klass, name, declared))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.reader(klass, name, declared)
|
|
65
|
+
proc do
|
|
66
|
+
target = klass.attribute_aliases.fetch(name, declared)
|
|
67
|
+
if is_a?(MaskedAttributes) && pf_masked_reader?(target)
|
|
68
|
+
read_attribute(target) { |missing| missing_attribute(missing, caller) }
|
|
69
|
+
else
|
|
70
|
+
_read_attribute(target) { |missing| missing_attribute(missing, caller) }
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def self.writer(klass, name, declared)
|
|
76
|
+
proc do |value|
|
|
77
|
+
target = klass.attribute_aliases.fetch(name, declared)
|
|
78
|
+
next if is_a?(MaskedAttributes) && pf_masked_column?(target) && pf_skip_masked_write?(target, value)
|
|
79
|
+
|
|
80
|
+
_write_attribute(target, value)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Rails 7.1: an alias calls the target's own reader or setter when the
|
|
86
|
+
# model defines it (deprecated there) or the target is not an
|
|
87
|
+
# attribute, and otherwise reads and writes the attribute as later
|
|
88
|
+
# versions do. So do these.
|
|
89
|
+
module TargetMethodAccessors
|
|
90
|
+
def self.define(klass, accessors, name, declared)
|
|
91
|
+
own = OwnMethods.new(klass, name, declared)
|
|
92
|
+
attribute_reader = AttributeAccessors.reader(klass, name, declared)
|
|
93
|
+
attribute_writer = AttributeAccessors.writer(klass, name, declared)
|
|
94
|
+
accessors.redefine_method(name) do
|
|
95
|
+
(reader = own.reader) ? send(reader) : instance_exec(&attribute_reader)
|
|
96
|
+
end
|
|
97
|
+
accessors.redefine_method(:"#{name}=") do |value|
|
|
98
|
+
(setter = own.setter) ? send(setter, value) : instance_exec(value, &attribute_writer)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# The target's methods an alias calls on Rails 7.1. The choice is
|
|
103
|
+
# Rails' own (`alias_attribute_method_definition`), made once per
|
|
104
|
+
# method on the class that declares the alias, with the concern's
|
|
105
|
+
# modules looked past: seen by Rails, the masked getter is a method
|
|
106
|
+
# the model defines.
|
|
107
|
+
class OwnMethods
|
|
108
|
+
def initialize(klass, name, declared)
|
|
109
|
+
@klass = klass
|
|
110
|
+
@name = name
|
|
111
|
+
@declared = declared
|
|
112
|
+
@choices = {}
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# @return [String, nil] the reader to call; nil to read the attribute
|
|
116
|
+
def reader
|
|
117
|
+
own(target)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# @return [String, nil] the setter to call; nil to write the attribute
|
|
121
|
+
def setter
|
|
122
|
+
own("#{target}=")
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
private
|
|
126
|
+
|
|
127
|
+
def target
|
|
128
|
+
@klass.attribute_aliases.fetch(@name, @declared)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def own(method_name)
|
|
132
|
+
@choices.fetch(method_name) { @choices[method_name] = (method_name if defined_by_the_model?(method_name)) }
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def defined_by_the_model?(method_name)
|
|
136
|
+
return true unless @klass.abstract_class? || @klass.has_attribute?(target)
|
|
137
|
+
return false if ActiveRecord::AttributeMethods.dangerous_attribute_methods.include?(method_name)
|
|
138
|
+
|
|
139
|
+
owner = owner_outside_the_concern(method_name)
|
|
140
|
+
!owner.nil? && !owner.is_a?(ActiveRecord::AttributeMethods::GeneratedAttributeMethods)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def owner_outside_the_concern(method_name)
|
|
144
|
+
method = @klass.instance_method(method_name)
|
|
145
|
+
method = method.super_method while method && concern_module?(method.owner)
|
|
146
|
+
method&.owner
|
|
147
|
+
rescue NameError
|
|
148
|
+
nil
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def concern_module?(owner)
|
|
152
|
+
[ColumnAccessors, VirtualReaders::ReaderModule, AccessorModule].any? { |kind| owner.is_a?(kind) }
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
class << self
|
|
158
|
+
# Defines the reader and the writer of one alias `klass` declares.
|
|
159
|
+
def define_accessors(klass, name, target)
|
|
160
|
+
target = target.to_s
|
|
161
|
+
return if target == UNMASKED_TARGET
|
|
162
|
+
|
|
163
|
+
accessors.define(klass, accessor_module(klass), name.to_s, target)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Covers the aliases `klass` itself declared, where Rails generates
|
|
167
|
+
# their methods: in `klass`, not in the classes that inherit them.
|
|
168
|
+
def mask_aliases_declared_by(klass)
|
|
169
|
+
declared_aliases(klass).each { |name, target| define_accessors(klass, name, target) }
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# @return [String, nil] the attribute `name` is an alias of on
|
|
173
|
+
# `klass`, following aliases of aliases
|
|
174
|
+
def target(klass, name)
|
|
175
|
+
aliases = klass.attribute_aliases
|
|
176
|
+
target = aliases[name.to_s]
|
|
177
|
+
target && chain_end(aliases, target)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# The attribute at the end of a chain of aliases that starts with
|
|
181
|
+
# `target`. Bounded, so aliases that point at each other end too.
|
|
182
|
+
def chain_end(aliases, target)
|
|
183
|
+
aliases.size.times { target = aliases.fetch(target) { return target } }
|
|
184
|
+
target
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
private
|
|
188
|
+
|
|
189
|
+
def accessors
|
|
190
|
+
@accessors ||= ActiveRecord.version < Gem::Version.new('7.2') ? TargetMethodAccessors : AttributeAccessors
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# The class's own module, prepended to the module Rails generates
|
|
194
|
+
# the class's attribute methods into.
|
|
195
|
+
def accessor_module(klass)
|
|
196
|
+
CREATION.synchronize do
|
|
197
|
+
next klass.const_get(:MaskedAliasAccessors, false) if klass.const_defined?(:MaskedAliasAccessors, false)
|
|
198
|
+
|
|
199
|
+
klass.const_set(:MaskedAliasAccessors, AccessorModule.new)
|
|
200
|
+
.tap { |accessors| klass.send(:generated_attribute_methods).prepend(accessors) }
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Rails keeps the aliases each class declares in a registry of its
|
|
205
|
+
# own; without one, the aliases that differ from the superclass's.
|
|
206
|
+
def declared_aliases(klass)
|
|
207
|
+
return registered_aliases(klass) if klass.respond_to?(:aliases_by_attribute_name)
|
|
208
|
+
|
|
209
|
+
inherited = klass.superclass.attribute_aliases
|
|
210
|
+
klass.attribute_aliases.reject { |name, target| inherited[name] == target }.to_a
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# The registry keeps the old target of an alias that was declared
|
|
214
|
+
# again with another one; only what the alias points to now counts.
|
|
215
|
+
def registered_aliases(klass)
|
|
216
|
+
current = klass.attribute_aliases
|
|
217
|
+
klass.aliases_by_attribute_name
|
|
218
|
+
.flat_map { |target, names| names.map { |name| [name, target] } }
|
|
219
|
+
.select { |name, target| current[name] == target }
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Extended into the model by ActiveSupport::Concern.
|
|
224
|
+
module ClassMethods
|
|
225
|
+
# The accessors are defined before Rails registers the alias, so
|
|
226
|
+
# they are in place however soon Rails generates its own.
|
|
227
|
+
def alias_attribute(new_name, old_name)
|
|
228
|
+
AliasAccess.define_accessors(self, new_name, old_name)
|
|
229
|
+
super
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
private
|
|
233
|
+
|
|
234
|
+
# For the aliases that were declared before the concern was
|
|
235
|
+
# included, each in the class that declared it: this class, its
|
|
236
|
+
# ancestors (which do not have the concern) and the subclasses that
|
|
237
|
+
# are loaded.
|
|
238
|
+
def pf_mask_attribute_aliases
|
|
239
|
+
lineage = ancestors.grep(Class).take_while { |klass| klass < ActiveRecord::Base }
|
|
240
|
+
(lineage + descendants).each { |klass| AliasAccess.mask_aliases_declared_by(klass) }
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# `methods:` names that are aliases of an attribute the current user
|
|
245
|
+
# may not read are dropped, the way the attribute itself is dropped
|
|
246
|
+
# from the attribute names. Only a loaded attribute is: reading an
|
|
247
|
+
# alias of one that was not loaded raises, as on any model.
|
|
248
|
+
#
|
|
249
|
+
# This module is included before MaskedAttributes itself (it is a
|
|
250
|
+
# dependency): while the concern is being included, on another
|
|
251
|
+
# thread, a record answers as it did without it.
|
|
252
|
+
def serializable_hash(options = nil)
|
|
253
|
+
return super unless is_a?(MaskedAttributes)
|
|
254
|
+
|
|
255
|
+
super(pf_without_hidden_alias_methods(options))
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
private
|
|
259
|
+
|
|
260
|
+
# The attribute an alias that points to `target` is judged as.
|
|
261
|
+
def pf_alias_end(target)
|
|
262
|
+
AliasAccess.chain_end(self.class.attribute_aliases, target)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def pf_without_hidden_alias_methods(options)
|
|
266
|
+
methods = options && options[:methods]
|
|
267
|
+
return options unless methods && (pf_guarded_user || pf_hide_without_user?) &&
|
|
268
|
+
PermissionForge.configuration.omit_hidden_in_attributes_hash
|
|
269
|
+
|
|
270
|
+
options.merge(methods: Array(methods).reject { |name| pf_hidden_alias?(name) })
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def pf_hidden_alias?(name)
|
|
274
|
+
target = AliasAccess.target(self.class, name)
|
|
275
|
+
target && has_attribute?(target) && pf_hidden_attribute?(target)
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Concerns
|
|
5
|
+
module MaskedAttributes
|
|
6
|
+
# Store accessors (`store :settings, accessors: [:theme]`,
|
|
7
|
+
# `store_accessor`) reach the store column through its public getter —
|
|
8
|
+
# the masked one. Left alone, a hidden or masked store column makes Rails
|
|
9
|
+
# see nil (or the mask) where it expects the stored hash: depending on
|
|
10
|
+
# the Rails version the read raises, or Rails replaces the column with an
|
|
11
|
+
# empty hash in memory and a later save wipes the stored data.
|
|
12
|
+
#
|
|
13
|
+
# - Reading a key of a store column the user may not read in full
|
|
14
|
+
# returns the hidden value and never touches the column.
|
|
15
|
+
# - Writing a key works on the real stored hash, so the other keys
|
|
16
|
+
# survive. The accessors and the column getter still return nothing
|
|
17
|
+
# readable afterwards.
|
|
18
|
+
#
|
|
19
|
+
# Only stores whose getter this concern masks are affected (a column, or
|
|
20
|
+
# a wrapped virtual reader while virtual masking is on); any other store
|
|
21
|
+
# is left to Rails.
|
|
22
|
+
#
|
|
23
|
+
# @api private
|
|
24
|
+
module StoreAccess
|
|
25
|
+
# Fiber-local ExecutionState slot: map (by identity) of record => the
|
|
26
|
+
# store column an accessor is writing. Kept beside the record, like
|
|
27
|
+
# the virtual-masking escape hatch, so a copy made during the write
|
|
28
|
+
# or another thread holding the record never reads the column raw.
|
|
29
|
+
RAW_STORE_KEY = :pf_raw_store_columns
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def read_store_attribute(store_attribute, key)
|
|
34
|
+
return super if pf_store_column_readable?(store_attribute) || pf_store_column_unloaded?(store_attribute)
|
|
35
|
+
|
|
36
|
+
PermissionForge.configuration.hidden_attribute_value
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def write_store_attribute(store_attribute, key, value)
|
|
40
|
+
pf_with_raw_store_column(store_attribute) { super }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# The attribute Rails reads the store through: a store may be declared
|
|
44
|
+
# on an alias_attribute name.
|
|
45
|
+
def pf_store_column_name(store_attribute)
|
|
46
|
+
name = store_attribute.to_s
|
|
47
|
+
self.class.attribute_aliases[name] || name
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Readable in full for the current user. Asked of the guard with a
|
|
51
|
+
# probe rather than by comparing stored values: a store that is nil
|
|
52
|
+
# (a NULL json column, a new record) must not look "readable".
|
|
53
|
+
def pf_store_column_readable?(store_attribute)
|
|
54
|
+
name = pf_store_column_name(store_attribute)
|
|
55
|
+
return true unless pf_masked_reader?(name)
|
|
56
|
+
|
|
57
|
+
include_field, value = pf_guard(name, VirtualReaders::PROBE)
|
|
58
|
+
include_field && value.equal?(VirtualReaders::PROBE)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# A store column the record was loaded without (`select(:id)`). Rails
|
|
62
|
+
# raises ActiveModel::MissingAttributeError when an accessor reads or
|
|
63
|
+
# writes one of its keys; hidden or not, it is left to do so.
|
|
64
|
+
def pf_store_column_unloaded?(store_attribute)
|
|
65
|
+
name = pf_store_column_name(store_attribute)
|
|
66
|
+
self.class.has_attribute?(name) && !has_attribute?(name)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Whether the reader of `name` is one this concern masks: the one Rails
|
|
70
|
+
# is about to call for a store, or the target of an alias (AliasAccess).
|
|
71
|
+
def pf_masked_reader?(name)
|
|
72
|
+
pf_masked_column?(name) || (pf_mask_virtual_reads? && self.class.pf_virtual_reader_masked?(name))
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# A column whose getter is masked and whose setter is guarded.
|
|
76
|
+
def pf_masked_column?(name)
|
|
77
|
+
klass = self.class
|
|
78
|
+
klass.const_defined?(:MaskedAttributeReaders) && klass::MaskedAttributeReaders.method_defined?(name)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Inside the block this record's getter for the store column returns
|
|
82
|
+
# the stored value (see MaskedAttributes#read_attribute). Only that
|
|
83
|
+
# column, only this record on this fiber, and restored when
|
|
84
|
+
# the block ends.
|
|
85
|
+
def pf_with_raw_store_column(store_attribute)
|
|
86
|
+
windows = PermissionForge::ExecutionState.fetch(RAW_STORE_KEY) { {}.compare_by_identity }
|
|
87
|
+
previous = windows[self]
|
|
88
|
+
windows[self] = pf_store_column_name(store_attribute)
|
|
89
|
+
yield
|
|
90
|
+
ensure
|
|
91
|
+
previous ? windows[self] = previous : windows.delete(self)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def pf_raw_store_column?(name)
|
|
95
|
+
PermissionForge::ExecutionState[RAW_STORE_KEY]&.[](self) == name
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Concerns
|
|
5
|
+
module MaskedAttributes
|
|
6
|
+
# Validations judge the stored value; what they report shows what the
|
|
7
|
+
# current user reads.
|
|
8
|
+
#
|
|
9
|
+
# A hidden or masked value would fail a presence or format check the
|
|
10
|
+
# stored value passes (nobody signed in under :hide, a user who reads
|
|
11
|
+
# `s***@acme.com` or nothing) and pass a uniqueness check against the
|
|
12
|
+
# mask. So while the validators run (run_validations!), the attribute
|
|
13
|
+
# Rails reads for a validation (read_attribute_for_validation) — that
|
|
14
|
+
# one attribute, nothing else of the record — reads as stored: a derived
|
|
15
|
+
# reader that reads other columns sees them masked, as always.
|
|
16
|
+
#
|
|
17
|
+
# Rails puts the value it validated into the error (value:, which
|
|
18
|
+
# errors.details returns and %{value} interpolates). When the
|
|
19
|
+
# validators are done, every error's value is replaced with what the
|
|
20
|
+
# current user's getter returns, or removed when the user may not read
|
|
21
|
+
# the attribute. A message rendered later reads the getter
|
|
22
|
+
# (read_attribute_for_validation outside the validators), so %{value}
|
|
23
|
+
# in a custom or i18n message shows the masked value or nothing.
|
|
24
|
+
#
|
|
25
|
+
# A Rails EachValidator reads the attribute through
|
|
26
|
+
# read_attribute_for_validation and then, in numericality, again with
|
|
27
|
+
# read_attribute (prepare_value_for_validation). So while an
|
|
28
|
+
# EachValidator judges this record (EachValidatorWindow), the attribute
|
|
29
|
+
# it last read for a validation stays readable as stored until the
|
|
30
|
+
# validator is done; anything else — another validator, a custom
|
|
31
|
+
# `validate` block — reads masked.
|
|
32
|
+
#
|
|
33
|
+
# @api private
|
|
34
|
+
module ValidationReads
|
|
35
|
+
# ExecutionState slots (fiber, by record identity): the records whose
|
|
36
|
+
# validators are running, and the attribute each is reading for one.
|
|
37
|
+
VALIDATING_KEY = :pf_validating_records
|
|
38
|
+
READS_KEY = :pf_validation_reads
|
|
39
|
+
# The records an EachValidator is judging right now.
|
|
40
|
+
WINDOWS_KEY = :pf_validator_windows
|
|
41
|
+
|
|
42
|
+
# Prepended to ActiveModel::EachValidator: keeps the attribute it
|
|
43
|
+
# read for a validation readable as stored until it is done.
|
|
44
|
+
module EachValidatorWindow
|
|
45
|
+
def validate(record)
|
|
46
|
+
return super unless record.is_a?(MaskedAttributes)
|
|
47
|
+
|
|
48
|
+
ValidationReads.within_validator(record) { super }
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @api private
|
|
53
|
+
def self.within_validator(record)
|
|
54
|
+
windows = PermissionForge::ExecutionState.fetch(WINDOWS_KEY) { Set.new.compare_by_identity }
|
|
55
|
+
added = windows.add?(record)
|
|
56
|
+
yield
|
|
57
|
+
ensure
|
|
58
|
+
if added
|
|
59
|
+
windows.delete(record)
|
|
60
|
+
PermissionForge::ExecutionState[READS_KEY]&.delete(record)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def read_attribute_for_validation(name)
|
|
65
|
+
window = pf_validator_window?
|
|
66
|
+
return super unless pf_validation_read_open?(window)
|
|
67
|
+
|
|
68
|
+
column = pf_validation_column(name)
|
|
69
|
+
pf_reading_for_validation(column || name.to_s, keep: window) do
|
|
70
|
+
column || !VirtualReaders.masked?(self.class, name) ? super : pf_without_virtual_masking { super }
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def run_validations!
|
|
77
|
+
validating = PermissionForge::ExecutionState.fetch(VALIDATING_KEY) { Set.new.compare_by_identity }
|
|
78
|
+
added = validating.add?(self)
|
|
79
|
+
super
|
|
80
|
+
ensure
|
|
81
|
+
validating&.delete(self) if added
|
|
82
|
+
pf_mask_error_values
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def pf_validating?
|
|
86
|
+
PermissionForge::ExecutionState[VALIDATING_KEY]&.include?(self) || false
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Validating, and no other validation read in progress — or inside an
|
|
90
|
+
# EachValidator, whose next read replaces the last.
|
|
91
|
+
def pf_validation_read_open?(window)
|
|
92
|
+
pf_validating? && (window || !pf_validation_read)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Marks `attribute` as read as stored for the block; inside an
|
|
96
|
+
# EachValidator (keep) it stays marked until the validator is done.
|
|
97
|
+
def pf_reading_for_validation(attribute, keep:)
|
|
98
|
+
reads = PermissionForge::ExecutionState.fetch(READS_KEY) { {}.compare_by_identity }
|
|
99
|
+
reads[self] = attribute
|
|
100
|
+
yield
|
|
101
|
+
ensure
|
|
102
|
+
reads&.delete(self) unless keep
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def pf_validator_window?
|
|
106
|
+
PermissionForge::ExecutionState[WINDOWS_KEY]&.include?(self) || false
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# The attribute a validator is reading on this record, or nil.
|
|
110
|
+
def pf_validation_read
|
|
111
|
+
PermissionForge::ExecutionState[READS_KEY]&.[](self)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# The column an attribute name reads (an alias as the attribute at
|
|
115
|
+
# the end of its chain), or nil for a method.
|
|
116
|
+
def pf_validation_column(name)
|
|
117
|
+
name = name.to_s
|
|
118
|
+
target = self.class.attribute_aliases[name]
|
|
119
|
+
name = pf_alias_end(target) if target
|
|
120
|
+
name if self.class.attribute_names.include?(name)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def pf_mask_error_values
|
|
124
|
+
errors.objects.each do |error|
|
|
125
|
+
options = error.options
|
|
126
|
+
next unless options.key?(:value) && error.attribute != :base
|
|
127
|
+
|
|
128
|
+
include_field, value = pf_error_value(error.attribute)
|
|
129
|
+
include_field ? options[:value] = value : options.delete(:value)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# [include?, value]: what the current user reads for the attribute,
|
|
134
|
+
# as a debugging read (no field.access decision). The primary key is
|
|
135
|
+
# never masked. Anything that fails leaves no value.
|
|
136
|
+
def pf_error_value(attribute)
|
|
137
|
+
column = pf_validation_column(attribute)
|
|
138
|
+
pf_while_inspecting do
|
|
139
|
+
if column
|
|
140
|
+
column == self.class.primary_key ? [true, _read_attribute(column)] : pf_guard(column, _read_attribute(column))
|
|
141
|
+
else
|
|
142
|
+
respond_to?(attribute) ? [true, public_send(attribute)] : [false, nil]
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
rescue StandardError
|
|
146
|
+
[false, nil]
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
ActiveModel::EachValidator.prepend(PermissionForge::Concerns::MaskedAttributes::ValidationReads::EachValidatorWindow)
|