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,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
# The gem's deprecations: one ActiveSupport::Deprecation for every renamed
|
|
5
|
+
# or removed public API (the README "Versioning and stability" surface).
|
|
6
|
+
#
|
|
7
|
+
# Convention: a public API is never renamed or removed in one step. The
|
|
8
|
+
# release that changes it keeps the old name working for at least one
|
|
9
|
+
# minor version, warning through {PermissionForge.deprecator}, and lists it
|
|
10
|
+
# under "Deprecated" in the CHANGELOG; the release at HORIZON removes it.
|
|
11
|
+
#
|
|
12
|
+
# # a renamed method or configuration option (reader and writer): the old
|
|
13
|
+
# # name calls the new one — not alias_method, which would skip the
|
|
14
|
+
# # writers Configuration prepends (validation, the preset's bookkeeping)
|
|
15
|
+
# def old_name = new_name
|
|
16
|
+
# def old_name=(value)
|
|
17
|
+
# self.new_name = value
|
|
18
|
+
# end
|
|
19
|
+
# PermissionForge.deprecator.deprecate_methods(self, old_name: :new_name, 'old_name=': :'new_name=')
|
|
20
|
+
# # a removed behaviour, warned once per process
|
|
21
|
+
# PermissionForge::Deprecation.warn('passing a String is deprecated; pass a Symbol')
|
|
22
|
+
#
|
|
23
|
+
# In a Rails application the deprecator is registered as
|
|
24
|
+
# `Rails.application.deprecators[:permission_forge]`, so the host's
|
|
25
|
+
# `config.active_support.deprecation` (:stderr, :log, :raise, :silence…),
|
|
26
|
+
# `disallowed_deprecation` and `Rails.application.deprecators.silence { }`
|
|
27
|
+
# govern it as they govern Rails' own.
|
|
28
|
+
#
|
|
29
|
+
# @api private
|
|
30
|
+
module Deprecation
|
|
31
|
+
# The version that removes what is deprecated now. A test fails once
|
|
32
|
+
# VERSION reaches it: remove the deprecated code, then move the horizon.
|
|
33
|
+
HORIZON = '0.15'
|
|
34
|
+
GEM_NAME = 'PermissionForge'
|
|
35
|
+
|
|
36
|
+
DEPRECATOR = ActiveSupport::Deprecation.new(HORIZON, GEM_NAME)
|
|
37
|
+
|
|
38
|
+
WARNED_MUTEX = Mutex.new
|
|
39
|
+
private_constant :WARNED_MUTEX
|
|
40
|
+
|
|
41
|
+
@warned = Set.new
|
|
42
|
+
|
|
43
|
+
class << self
|
|
44
|
+
# @return [ActiveSupport::Deprecation]
|
|
45
|
+
def deprecator = DEPRECATOR
|
|
46
|
+
|
|
47
|
+
# Warns through the deprecator the first time `key` is seen in this
|
|
48
|
+
# process, so a deprecated call on a hot path does not flood the log.
|
|
49
|
+
# A warning that raised (behavior :raise) or was silenced is not
|
|
50
|
+
# remembered: the next call warns (or raises) again. The message says
|
|
51
|
+
# what to use instead; the version that removes it is added.
|
|
52
|
+
#
|
|
53
|
+
# @param message [String] e.g. "passing a String is deprecated; pass a Symbol"
|
|
54
|
+
# @param key [Object] what "once" is counted by (default: the message)
|
|
55
|
+
# @return [void]
|
|
56
|
+
def warn(message, key: message, callstack: caller_locations(1))
|
|
57
|
+
return if WARNED_MUTEX.synchronize { @warned.include?(key) }
|
|
58
|
+
|
|
59
|
+
DEPRECATOR.warn("#{message}. #{GEM_NAME} #{HORIZON} removes it.", callstack)
|
|
60
|
+
WARNED_MUTEX.synchronize { @warned << key } unless DEPRECATOR.silenced
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Forgets which keys warned (for tests that check a warning).
|
|
64
|
+
#
|
|
65
|
+
# @return [void]
|
|
66
|
+
def reset!
|
|
67
|
+
WARNED_MUTEX.synchronize { @warned.clear }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Hands the deprecator to the application's deprecators (Rails 7.1+).
|
|
71
|
+
#
|
|
72
|
+
# @param app [Rails::Application]
|
|
73
|
+
# @return [void]
|
|
74
|
+
def register(app)
|
|
75
|
+
app.deprecators[:permission_forge] = DEPRECATOR if app.respond_to?(:deprecators)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# @return [ActiveSupport::Deprecation] the gem's deprecator
|
|
81
|
+
def self.deprecator = Deprecation.deprecator
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Runs when the application starts initializing, or at once when it already
|
|
85
|
+
# has (the gem required late).
|
|
86
|
+
ActiveSupport.on_load(:before_initialize, yield: true) { |app| PermissionForge::Deprecation.register(app) }
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
require_relative 'query_scoping_warnings'
|
|
2
|
+
require_relative 'boot_checks'
|
|
3
|
+
require_relative 'masking_sync'
|
|
4
|
+
require_relative 'masked_models'
|
|
5
|
+
|
|
6
|
+
module PermissionForge
|
|
7
|
+
# Wires the gem into the host application: initializers, the executor and
|
|
8
|
+
# reloader hooks, the boot checks. The work is done by plain modules
|
|
9
|
+
# (BootChecks, MaskingSync, Audit, ...).
|
|
10
|
+
#
|
|
11
|
+
# @api public
|
|
12
|
+
class Engine < ::Rails::Engine
|
|
13
|
+
isolate_namespace PermissionForge
|
|
14
|
+
|
|
15
|
+
config.generators do |g|
|
|
16
|
+
g.test_framework :minitest
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# The models (app/models/permission_forge) are autoloaded like any engine
|
|
20
|
+
# model: their class bodies read the configuration after the host's
|
|
21
|
+
# config/initializers, so nothing is re-declared after boot. No engine
|
|
22
|
+
# initializer may reference them — the application's autoloader is set up
|
|
23
|
+
# after every initializer has run (Rails 7.1+).
|
|
24
|
+
|
|
25
|
+
# The admin UI's CSS/JS/brand files are served by PermissionForge::AssetsController
|
|
26
|
+
# straight from app/frontend — no Sprockets/Propshaft/importmap registration needed.
|
|
27
|
+
|
|
28
|
+
initializer 'permission_forge.i18n' do
|
|
29
|
+
locale_dir = root.join('config', 'locales')
|
|
30
|
+
allowed = PermissionForge.configuration.available_locales || PermissionForge::BUNDLED_LOCALES
|
|
31
|
+
|
|
32
|
+
if allowed.present?
|
|
33
|
+
pattern = "{#{Array(allowed).join(',')}}"
|
|
34
|
+
config.i18n.load_path += Dir[locale_dir.join("permission_forge.#{pattern}.yml")]
|
|
35
|
+
else
|
|
36
|
+
config.i18n.load_path += Dir[locale_dir.join('permission_forge.*.yml')]
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
initializer 'permission_forge.append_routes' do |_app|
|
|
41
|
+
# Routes are only active when the engine is mounted — no automatic injection
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
initializer 'permission_forge.filter_parameters', after: :load_config_initializers do |app|
|
|
45
|
+
pf_config = PermissionForge.configuration
|
|
46
|
+
if pf_config.gdpr_defaults && pf_config.filter_parameters_integration
|
|
47
|
+
new_filters = PermissionForge.gdpr_filter_parameters
|
|
48
|
+
app.config.filter_parameters += new_filters
|
|
49
|
+
|
|
50
|
+
Rails.logger.info "[PermissionForge] Added #{new_filters.size} GDPR patterns to filter_parameters."
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Tenancy needs the acts_as_tenant gem: refuse to boot without it. The
|
|
55
|
+
# scope itself is declared by each model as it loads (HasTenantScoping).
|
|
56
|
+
initializer 'permission_forge.tenant_scoping', after: :load_config_initializers do
|
|
57
|
+
PermissionForge::BootChecks.ensure_tenancy_gem!(PermissionForge.configuration)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Audit event log: install the notification subscribers (Audit::Subscribers
|
|
61
|
+
# — log, store and legacy-hook adapters register there) and reset the
|
|
62
|
+
# audit context at the end of every executor run.
|
|
63
|
+
initializer 'permission_forge.audit', after: :load_config_initializers do |app|
|
|
64
|
+
PermissionForge::Audit.install!(app)
|
|
65
|
+
PermissionForge::Audit::JobContext.install!
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Level 2 — install the unscoped-query detection subscriber once config is
|
|
69
|
+
# final. The per-event check re-reads config, so the subscriber is only
|
|
70
|
+
# installed when detection could ever be active (avoids a no-op subscriber
|
|
71
|
+
# on every query in production).
|
|
72
|
+
initializer 'permission_forge.unscoped_query_detection', after: :load_config_initializers do
|
|
73
|
+
PermissionForge::Services::UnscopedQueryDetector.install! if PermissionForge.configuration.unscoped_query_detection != :off
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Auto-applied masking in every process (MaskingSync): a check at the
|
|
77
|
+
# start of each request and job, and a re-apply after a code reload.
|
|
78
|
+
# Installed whatever the settings say now: one may be switched on after
|
|
79
|
+
# the boot step (check! costs nothing while both are off).
|
|
80
|
+
initializer 'permission_forge.masking_sync' do |app|
|
|
81
|
+
app.executor.to_run { PermissionForge::MaskingSync.check! }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
config.to_prepare { PermissionForge::MaskingSync.reloaded! }
|
|
85
|
+
|
|
86
|
+
config.after_initialize do
|
|
87
|
+
PermissionForge::BootChecks.run(PermissionForge.configuration)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# A setting a model reads as it loads (user_class, audit_user_class,
|
|
91
|
+
# tenant_model) changed by the host's own after_initialize, once a
|
|
92
|
+
# production boot has eager-loaded the models: the classes keep what they
|
|
93
|
+
# declared (a development process would pick the change up). Registered
|
|
94
|
+
# after config/initializers so it runs after the after_initialize blocks
|
|
95
|
+
# declared there; nothing loads a model.
|
|
96
|
+
initializer 'permission_forge.model_bindings', after: :load_config_initializers do |app|
|
|
97
|
+
app.config.after_initialize { PermissionForge::ModelBindings.warn! }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Deprecated: the masking machinery left the Engine (0.14). Removed in
|
|
101
|
+
# PermissionForge::Deprecation::HORIZON.
|
|
102
|
+
#
|
|
103
|
+
# @return [Boolean] what MaskedModels.apply returns
|
|
104
|
+
def self.apply_masked_attributes_to_managed_models(only: nil)
|
|
105
|
+
PermissionForge::Deprecation.warn(
|
|
106
|
+
'PermissionForge::Engine.apply_masked_attributes_to_managed_models is deprecated; ' \
|
|
107
|
+
'use PermissionForge::MaskedModels.apply(only:)',
|
|
108
|
+
callstack: caller_locations(1) # the host's call, not this method
|
|
109
|
+
)
|
|
110
|
+
PermissionForge::MaskedModels.apply(only: only)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
# The one place the gem keeps state for the code running right now: a
|
|
5
|
+
# flag set inside a block, the record being read, the build in progress.
|
|
6
|
+
# Nothing else in lib/ or app/ touches `Thread.current`, `Fiber[]`,
|
|
7
|
+
# ActiveSupport::IsolatedExecutionState or a CurrentAttributes class
|
|
8
|
+
# (test/permission_forge/execution_state_test.rb fails when something
|
|
9
|
+
# does; its ALLOWED list names the exceptions and why).
|
|
10
|
+
#
|
|
11
|
+
# Every slot is listed in KEYS with what it holds, where it is kept and
|
|
12
|
+
# what the end of an executor run (a request, a job) does to it.
|
|
13
|
+
#
|
|
14
|
+
# Where (`scope`):
|
|
15
|
+
#
|
|
16
|
+
# - :fiber — the running fiber, whatever Rails' isolation level says
|
|
17
|
+
# (Thread#[], which is fiber-local). For every slot whose value,
|
|
18
|
+
# seen by code it was not set for, would widen access (a scope
|
|
19
|
+
# suspended, a mask lifted, a re-entrant build answered with {}) or
|
|
20
|
+
# drop audit events (an emission or a read in progress): another
|
|
21
|
+
# fiber of the thread — an Enumerator iterated with #next, a
|
|
22
|
+
# streaming template — never sees it, and a fiber left paused inside
|
|
23
|
+
# the block takes it with it.
|
|
24
|
+
# - :execution — ActiveSupport::IsolatedExecutionState: per thread
|
|
25
|
+
# under Rails' `:thread` isolation (the fibers of the thread share
|
|
26
|
+
# it, as they share ActiveRecord's connection and CurrentAttributes),
|
|
27
|
+
# per fiber under `:fiber`. For state that has to be seen wherever
|
|
28
|
+
# the request's code runs: the audit context, the open invalidation
|
|
29
|
+
# batch of the connection, and the cache failures (a failure reported
|
|
30
|
+
# in an Enumerator's fiber must still count for the Failure.none?
|
|
31
|
+
# around it).
|
|
32
|
+
#
|
|
33
|
+
# At the end of a run (`at_run_end`):
|
|
34
|
+
#
|
|
35
|
+
# - :clean — what the run added is removed, and nothing else: a slot
|
|
36
|
+
# that was empty when the run started is emptied, a collection that
|
|
37
|
+
# was there loses the entries the run added, and a slot that held a
|
|
38
|
+
# value is left alone. A request or job run starts empty, so there
|
|
39
|
+
# the slot is emptied, and nothing a request left set reaches the
|
|
40
|
+
# next one on the thread. Nothing is ever written back: a run may
|
|
41
|
+
# complete after the block it started in has ended
|
|
42
|
+
# (ActionDispatch::Executor completes it when the server closes the
|
|
43
|
+
# body, after the middlewares above it returned), and writing the
|
|
44
|
+
# block's value back then would leave it set for good. A block that
|
|
45
|
+
# wraps the whole run (a console or rake block around an inline job)
|
|
46
|
+
# keeps its value because it is still there.
|
|
47
|
+
# - :keep — outlives runs: a counter only ever compared before and
|
|
48
|
+
# after one call, or a slot whose block puts back what it found and
|
|
49
|
+
# which, emptied in the middle of the block, would fail open.
|
|
50
|
+
#
|
|
51
|
+
# A run that completes inside a block that started during the run — a
|
|
52
|
+
# `run!(reset: true)` completing an outer run (ActionDispatch::Executor
|
|
53
|
+
# does that for a run left open) — empties that block's slot for the
|
|
54
|
+
# rest of the block: it fails closed (scoping applies again).
|
|
55
|
+
#
|
|
56
|
+
# @api private
|
|
57
|
+
module ExecutionState
|
|
58
|
+
# One slot: its name, where it is kept (:fiber or :execution), what the
|
|
59
|
+
# end of an executor run does to it (:clean or :keep) and what it holds.
|
|
60
|
+
Key = Data.define(:name, :scope, :at_run_end, :holds)
|
|
61
|
+
|
|
62
|
+
SCOPES = %i[fiber execution].freeze
|
|
63
|
+
AT_RUN_END = %i[clean keep].freeze
|
|
64
|
+
|
|
65
|
+
KEYS = [
|
|
66
|
+
Key.new(:pf_skip_auto_scope, :fiber, :clean,
|
|
67
|
+
'true inside PermissionForge.without_permission_scope (and the internal builds): Level 1 auto-scoping ' \
|
|
68
|
+
'and Level 2 detection are suspended. Fiber: seen elsewhere it would lift scoping there'),
|
|
69
|
+
Key.new(:pf_internal_scope_suspension, :fiber, :clean,
|
|
70
|
+
'true while the gem suspends Level 1 for its own queries (AutoPermissionScoping.internally_suspended: ' \
|
|
71
|
+
'find_by / permission_scoped bodies, invalidation holders, cache clears, audit lookups): not reported as ' \
|
|
72
|
+
'a scope_suspended bypass, and nobody signed in gets no find_by bypass. Same scope as pf_skip_auto_scope ' \
|
|
73
|
+
'(host_suspended? reads both). Fiber: seen elsewhere it would hide a bypass from the audit log'),
|
|
74
|
+
Key.new(:pf_in_auto_scope_hook, :fiber, :clean,
|
|
75
|
+
'true while the Level 1 default scope builds the rules: a query made meanwhile (current_user_resolver, a ' \
|
|
76
|
+
'permissions rebuild) runs unscoped instead of recursing. Fiber: seen elsewhere every query runs unscoped'),
|
|
77
|
+
Key.new(:pf_building_permission_keys, :fiber, :clean,
|
|
78
|
+
'Hash of permission cache key => the read-through build (or request-context resolution) in progress; a ' \
|
|
79
|
+
'read of a key being built gets the reentrant {}. Fiber: seen elsewhere a read gets {} (rules off)'),
|
|
80
|
+
Key.new(:pf_raw_store_columns, :fiber, :clean,
|
|
81
|
+
'Hash (by identity) of record => the store column a store accessor is writing. Fiber: seen elsewhere ' \
|
|
82
|
+
'the column reads unmasked'),
|
|
83
|
+
Key.new(:pf_validating_records, :fiber, :clean,
|
|
84
|
+
'Set (by identity) of the records whose validators are running (MaskedAttributes::ValidationReads). ' \
|
|
85
|
+
'Fiber: seen elsewhere a validation read there would read stored values'),
|
|
86
|
+
Key.new(:pf_validator_windows, :fiber, :clean,
|
|
87
|
+
'Set (by identity) of the records a Rails EachValidator is judging: the attribute it read for the ' \
|
|
88
|
+
'validation stays readable as stored until it is done (numericality reads it twice). Fiber: seen ' \
|
|
89
|
+
'elsewhere that attribute reads unmasked'),
|
|
90
|
+
Key.new(:pf_validation_reads, :fiber, :clean,
|
|
91
|
+
'Hash (by identity) of record => the attribute a validator is reading, which reads as stored. Fiber: ' \
|
|
92
|
+
'seen elsewhere that column reads unmasked'),
|
|
93
|
+
Key.new(:pf_unmasked_virtual_records, :fiber, :clean,
|
|
94
|
+
'Set (by identity) of the records inside pf_without_virtual_masking. Fiber: seen elsewhere the virtual ' \
|
|
95
|
+
'readers read unmasked'),
|
|
96
|
+
Key.new(:pf_audit_emitting, :fiber, :clean,
|
|
97
|
+
'true while a runtime audit event is built, stored or published (re-entrancy guard). Fiber: seen ' \
|
|
98
|
+
'elsewhere no runtime event is emitted there'),
|
|
99
|
+
Key.new(:pf_audit_field_access, :fiber, :clean,
|
|
100
|
+
'the Audit::FieldAccess::Frame of the record read in progress. Fiber: seen elsewhere its decisions go ' \
|
|
101
|
+
'into a frame that is not theirs'),
|
|
102
|
+
Key.new(:pf_inspecting, :fiber, :clean,
|
|
103
|
+
'true while inspect / pretty_print read a record: no field.access decision. Fiber: seen elsewhere ' \
|
|
104
|
+
'field reads go unrecorded'),
|
|
105
|
+
Key.new(:pf_open_invalidation_batch, :execution, :clean,
|
|
106
|
+
'the InvalidationBatch the next permission record written on its connection joins. Execution, like the ' \
|
|
107
|
+
'connection: losing or sharing a batch costs repeated invalidations, never a missed one'),
|
|
108
|
+
Key.new(:pf_cache_failure_collectors, :execution, :keep,
|
|
109
|
+
'stack of Cache::Failure::Collected, one per Cache::Failure.collect block. Execution: a failure ' \
|
|
110
|
+
'reported in a fiber of the block is collected. Kept at the end of a run: each collect removes its ' \
|
|
111
|
+
'own entry by identity, and emptying the stack inside a block would lose its failure (fails open)'),
|
|
112
|
+
Key.new(:pf_cache_failures, :execution, :keep,
|
|
113
|
+
'Integer count of the cache failures reported (Cache::Failure.none? compares it before and after). ' \
|
|
114
|
+
'Execution: a failure in a fiber of the block counts; never reset, so a none? around a run sees one in it'),
|
|
115
|
+
Key.new(:pf_store_errors, :execution, :keep,
|
|
116
|
+
'Array of the errors a cache store handed its error handler during the store call in progress ' \
|
|
117
|
+
'(Cache::HandledErrors.during, .written). Execution: another fiber\'s error seen here counts against the call ' \
|
|
118
|
+
'(it is tried again). Kept at the end of a run: the block puts back what it found, and emptying the ' \
|
|
119
|
+
'slot inside the call would lose the error (a failed write taken for done)'),
|
|
120
|
+
Key.new(:pf_audit_context, :execution, :clean,
|
|
121
|
+
'the frozen Hash of the Audit::Context slots (actor, source, request id, IP, reason, correlation id)'),
|
|
122
|
+
Key.new(:pf_audit_transaction_correlation, :execution, :clean,
|
|
123
|
+
'[transaction, id] of the correlation id minted for the outermost open transaction'),
|
|
124
|
+
Key.new(:pf_audit_runtime_once, :execution, :clean,
|
|
125
|
+
'Hash of the runtime audit events published once per request (Audit::Runtime.once?)')
|
|
126
|
+
].to_h { |key| [key.name, key] }.freeze
|
|
127
|
+
|
|
128
|
+
CLEANED = KEYS.values.select { |key| key.at_run_end == :clean }.map(&:name).freeze
|
|
129
|
+
|
|
130
|
+
# The :fiber store: Thread#[] is fiber-local.
|
|
131
|
+
module FiberStorage
|
|
132
|
+
class << self
|
|
133
|
+
def [](name)
|
|
134
|
+
Thread.current[name]
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def []=(name, value)
|
|
138
|
+
Thread.current[name] = value
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Ruby drops a fiber-local set to nil.
|
|
142
|
+
def delete(name)
|
|
143
|
+
locals = Thread.current
|
|
144
|
+
value = locals[name]
|
|
145
|
+
locals[name] = nil unless value.nil?
|
|
146
|
+
value
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
STORAGE = KEYS.transform_values { |key| key.scope == :fiber ? FiberStorage : ActiveSupport::IsolatedExecutionState }.freeze
|
|
152
|
+
|
|
153
|
+
# The :clean slots by scope: what the start and the end of a run walk.
|
|
154
|
+
CLEANED_FIBER = CLEANED.select { |name| KEYS[name].scope == :fiber }.freeze
|
|
155
|
+
CLEANED_EXECUTION = (CLEANED - CLEANED_FIBER).freeze
|
|
156
|
+
|
|
157
|
+
# The executor hook (Rails.application.executor.register_hook): notes
|
|
158
|
+
# what the :clean slots hold when a run starts, and removes what the run
|
|
159
|
+
# added when it completes.
|
|
160
|
+
module RunHook
|
|
161
|
+
# The fiber and the execution context the run started on, and what the
|
|
162
|
+
# :clean slots held then (ExecutionState.snapshot; nil for nothing).
|
|
163
|
+
Snapshot = Data.define(:fiber, :context, :values)
|
|
164
|
+
|
|
165
|
+
class << self
|
|
166
|
+
# @return [Snapshot]
|
|
167
|
+
def run
|
|
168
|
+
Snapshot.new(Fiber.current, ActiveSupport::IsolatedExecutionState.context, ExecutionState.snapshot)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Only where the run started: its fiber (:fiber slots) or its
|
|
172
|
+
# execution context (:execution slots).
|
|
173
|
+
#
|
|
174
|
+
# @param snapshot [Snapshot]
|
|
175
|
+
# @return [void]
|
|
176
|
+
def complete(snapshot)
|
|
177
|
+
ExecutionState.run_completed(snapshot.values, fiber: snapshot.fiber.equal?(Fiber.current),
|
|
178
|
+
execution: snapshot.context.equal?(ActiveSupport::IsolatedExecutionState.context))
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
class << self
|
|
184
|
+
# @param name [Symbol] a key of KEYS
|
|
185
|
+
# @return [Object, nil]
|
|
186
|
+
def [](name)
|
|
187
|
+
storage(name)[name]
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# @param name [Symbol] a key of KEYS
|
|
191
|
+
# @return [Object] the value
|
|
192
|
+
def []=(name, value)
|
|
193
|
+
storage(name)[name] = value
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# The slot's value; when it holds nothing, the block's value, stored.
|
|
197
|
+
#
|
|
198
|
+
# ExecutionState.fetch(:pf_unmasked_virtual_records) { Set.new.compare_by_identity }
|
|
199
|
+
#
|
|
200
|
+
# @param name [Symbol] a key of KEYS
|
|
201
|
+
# @return [Object]
|
|
202
|
+
def fetch(name)
|
|
203
|
+
value = self[name]
|
|
204
|
+
value.nil? ? (self[name] = yield) : value
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# @param name [Symbol] a key of KEYS
|
|
208
|
+
# @return [Object, nil] what the slot held
|
|
209
|
+
def delete(name)
|
|
210
|
+
store = storage(name)
|
|
211
|
+
store.delete(name)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Sets the slot for the block and puts back what it held when the
|
|
215
|
+
# block ends, however it ends (a slot that held nil is emptied).
|
|
216
|
+
#
|
|
217
|
+
# @param name [Symbol] a key of KEYS
|
|
218
|
+
# @return the block's value
|
|
219
|
+
def with(name, value)
|
|
220
|
+
store = storage(name)
|
|
221
|
+
previous = store[name]
|
|
222
|
+
store[name] = value
|
|
223
|
+
begin
|
|
224
|
+
yield
|
|
225
|
+
ensure
|
|
226
|
+
previous.nil? ? store.delete(name) : store[name] = previous
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Empties every slot of this fiber and execution context: tests.
|
|
231
|
+
#
|
|
232
|
+
# @return [void]
|
|
233
|
+
def reset!
|
|
234
|
+
KEYS.each_key { |name| delete(name) }
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Registers RunHook with the application's executor (every request and
|
|
238
|
+
# job). Called once by Audit.install!.
|
|
239
|
+
#
|
|
240
|
+
# @param app [Rails::Application]
|
|
241
|
+
# @return [void]
|
|
242
|
+
def install!(app)
|
|
243
|
+
app.executor.register_hook(RunHook)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# What the :clean slots hold now (RunHook): nil when they are all
|
|
247
|
+
# empty (a request or a job), else name => [value, copy of its entries
|
|
248
|
+
# when it is a collection changed in place].
|
|
249
|
+
#
|
|
250
|
+
# @return [Hash{Symbol => Array(Object, Object)}, nil]
|
|
251
|
+
def snapshot
|
|
252
|
+
values = nil
|
|
253
|
+
locals = Thread.current
|
|
254
|
+
CLEANED_FIBER.each { |name| values = noted(values, name, locals[name]) }
|
|
255
|
+
CLEANED_EXECUTION.each { |name| values = noted(values, name, ActiveSupport::IsolatedExecutionState[name]) }
|
|
256
|
+
values
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# The end of an executor run (RunHook): removes from the :clean slots of
|
|
260
|
+
# the scopes the run ends in what the run added, and never writes a
|
|
261
|
+
# value. A slot empty at the start (`values` from #snapshot) is
|
|
262
|
+
# emptied; from a collection there at the start, the entries the run
|
|
263
|
+
# added are dropped; any other slot is left as it is.
|
|
264
|
+
#
|
|
265
|
+
# @param values [Hash, nil] from #snapshot
|
|
266
|
+
# @return [void]
|
|
267
|
+
def run_completed(values, fiber:, execution:)
|
|
268
|
+
clean_all(CLEANED_FIBER, FiberStorage, values) if fiber
|
|
269
|
+
clean_all(CLEANED_EXECUTION, ActiveSupport::IsolatedExecutionState, values) if execution
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
private
|
|
273
|
+
|
|
274
|
+
def noted(values, name, value)
|
|
275
|
+
return values if value.nil?
|
|
276
|
+
|
|
277
|
+
(values || {}).merge!(name => [value, collection?(value) ? value.dup : nil])
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def clean_all(names, store, values)
|
|
281
|
+
return names.each { |name| store.delete(name) } unless values
|
|
282
|
+
|
|
283
|
+
names.each { |name| (before = values[name]) ? clean(store, name, before) : store.delete(name) }
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# A slot that held `value` when the run started: only a collection
|
|
287
|
+
# still in it loses what the run added.
|
|
288
|
+
def clean(store, name, before)
|
|
289
|
+
value, entries = before
|
|
290
|
+
return unless entries
|
|
291
|
+
|
|
292
|
+
current = store[name]
|
|
293
|
+
drop_added(current, entries) if current.equal?(value)
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Drops the entries of `collection` that `before` (its copy from the
|
|
297
|
+
# start of the run) does not have.
|
|
298
|
+
def drop_added(collection, before)
|
|
299
|
+
case collection
|
|
300
|
+
when Hash then collection.select! { |key, _| before.key?(key) }
|
|
301
|
+
when Set then collection.select! { |entry| before.include?(entry) }
|
|
302
|
+
else collection.select! { |entry| before.any? { |kept| kept.equal?(entry) } }
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def storage(name)
|
|
307
|
+
STORAGE.fetch(name) do
|
|
308
|
+
raise ArgumentError, "unknown execution state key #{name.inspect} (PermissionForge::ExecutionState::KEYS)"
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# A collection changed in place (a frozen one is replaced, never changed).
|
|
313
|
+
def collection?(value)
|
|
314
|
+
(value.is_a?(Hash) || value.is_a?(Set) || value.is_a?(Array)) && !value.frozen?
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
end
|