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,576 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'masked_attributes/virtual_readers'
|
|
4
|
+
require_relative 'masked_attributes/store_access'
|
|
5
|
+
require_relative 'masked_attributes/alias_access'
|
|
6
|
+
require_relative 'masked_attributes/validation_reads'
|
|
7
|
+
|
|
8
|
+
module PermissionForge
|
|
9
|
+
module Concerns
|
|
10
|
+
# Auto-applies field-level read permissions on the model itself by overriding
|
|
11
|
+
# `read_attribute`, `attributes`, every per-column getter, and the
|
|
12
|
+
# `*_for_serialization` hooks. Any path that touches a column — direct getter
|
|
13
|
+
# (`customer.email`), bracket access (`customer[:email]`), `attributes`,
|
|
14
|
+
# `as_json`, `to_json`, `serializable_hash`, AMS, Jbuilder — is enforced.
|
|
15
|
+
#
|
|
16
|
+
# Hidden attributes are *omitted* from `attributes` by default — matching
|
|
17
|
+
# Salesforce `Security.stripInaccessible()` semantics. Configure via
|
|
18
|
+
# {PermissionForge::Configuration#omit_hidden_in_attributes_hash}.
|
|
19
|
+
#
|
|
20
|
+
# NOTE: This does NOT intercept `pluck`, raw SQL, or any path that bypasses
|
|
21
|
+
# the model layer. Use {PermissionForge::Services::FiltersResponseAttributes}
|
|
22
|
+
# for those code paths.
|
|
23
|
+
#
|
|
24
|
+
# Virtual (non-column) readers are covered by {VirtualReaders}, opt-in
|
|
25
|
+
# through config.mask_virtual_attributes. An alias_attribute name reads and
|
|
26
|
+
# writes its attribute, at that attribute's level ({AliasAccess}).
|
|
27
|
+
#
|
|
28
|
+
# With no current user every value is returned as stored, unless
|
|
29
|
+
# config.masked_attributes_nil_user_strategy = :hide, which hides every
|
|
30
|
+
# guarded attribute instead (the primary key and the raw paths excepted).
|
|
31
|
+
#
|
|
32
|
+
# @example Include in a model
|
|
33
|
+
# class Customer < ApplicationRecord
|
|
34
|
+
# include PermissionForge::Concerns::PermissionScopedFindable
|
|
35
|
+
# include PermissionForge::Concerns::MaskedAttributes
|
|
36
|
+
# end
|
|
37
|
+
#
|
|
38
|
+
# customer.email # masked / nil / raw based on current_user
|
|
39
|
+
# customer.read_attribute(:email) # same
|
|
40
|
+
# customer.as_json # hidden keys omitted entirely
|
|
41
|
+
#
|
|
42
|
+
# @api public
|
|
43
|
+
module MaskedAttributes # rubocop:disable Metrics/ModuleLength
|
|
44
|
+
extend ActiveSupport::Concern
|
|
45
|
+
|
|
46
|
+
# The class methods the guard calls. ActiveSupport::Concern extends a
|
|
47
|
+
# concern's own class methods only after it has included the concern,
|
|
48
|
+
# and another thread can read a record of the class in between, through
|
|
49
|
+
# the read_attribute just included. A dependency's class methods are
|
|
50
|
+
# extended before: so they live here.
|
|
51
|
+
#
|
|
52
|
+
# @api private
|
|
53
|
+
module ResourceName
|
|
54
|
+
extend ActiveSupport::Concern
|
|
55
|
+
|
|
56
|
+
# Extended into the model by ActiveSupport::Concern.
|
|
57
|
+
module ClassMethods
|
|
58
|
+
# The Resource name a read of this class is reported under, as a
|
|
59
|
+
# Symbol: worked out once per class, not on every read.
|
|
60
|
+
def pf_resource_symbol
|
|
61
|
+
@pf_resource_symbol ||= name.to_sym
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The Resource names a field of this class is looked up under: its
|
|
65
|
+
# own and its STI ancestors' (ResourceChain), worked out once per
|
|
66
|
+
# class. A read is reported under the first of them.
|
|
67
|
+
def pf_resource_chain
|
|
68
|
+
PermissionForge::ResourceChain.names(self)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
include ResourceName
|
|
74
|
+
include VirtualReaders
|
|
75
|
+
include StoreAccess
|
|
76
|
+
include AliasAccess
|
|
77
|
+
include ValidationReads
|
|
78
|
+
|
|
79
|
+
# Raised (when config.masked_write_protection == :raise) if a masked or hidden
|
|
80
|
+
# value read through the getter is written back to the same attribute.
|
|
81
|
+
#
|
|
82
|
+
# @api public
|
|
83
|
+
class MaskedWriteError < StandardError
|
|
84
|
+
def initialize(attribute)
|
|
85
|
+
super("Refusing to persist a masked/hidden value back to '#{attribute}'. " \
|
|
86
|
+
'This usually means a callback read the masked value and wrote it back. ' \
|
|
87
|
+
'Read the raw value via read_attribute_before_type_cast, or set ' \
|
|
88
|
+
'config.masked_write_protection = :skip.')
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# The module a class's masked column getters and guarded setters live in.
|
|
93
|
+
#
|
|
94
|
+
# @api private
|
|
95
|
+
class ColumnAccessors < Module; end
|
|
96
|
+
|
|
97
|
+
# Set (an ExecutionState slot) while attribute_for_inspect reads: a
|
|
98
|
+
# debugging read is not a field read.
|
|
99
|
+
INSPECT_KEY = :pf_inspecting
|
|
100
|
+
|
|
101
|
+
included do
|
|
102
|
+
next if PermissionForge::Concerns::MaskedAttributes.applied?(self)
|
|
103
|
+
|
|
104
|
+
# AR generates per-column reader methods on a dedicated module
|
|
105
|
+
# (`generated_attribute_methods`) and includes it in the model. Calling
|
|
106
|
+
# `customer.email` hits that fast path, bypassing `read_attribute`. We
|
|
107
|
+
# prepend an override module so direct getters (and setters, for the
|
|
108
|
+
# write-back guard) route through our logic. Captured at include time —
|
|
109
|
+
# column changes after this require a class reload to pick up.
|
|
110
|
+
#
|
|
111
|
+
# A getter raises ActiveModel::MissingAttributeError for a column that
|
|
112
|
+
# was not loaded (`select(:id)`), as the getter Rails generates does.
|
|
113
|
+
#
|
|
114
|
+
# Primary key is intentionally never overridden — it's used by routing,
|
|
115
|
+
# association lookups, and tenant scoping (acts_as_tenant calls `id` on
|
|
116
|
+
# the tenant model when computing cache keys). Masking `id` would create
|
|
117
|
+
# a recursion: `id` → guard → `permissions_hash` → `current_tenant.id`.
|
|
118
|
+
masked_attribute_module = ColumnAccessors.new
|
|
119
|
+
primary_key = self.primary_key&.to_s
|
|
120
|
+
columns_hash.each_key do |col_name|
|
|
121
|
+
next if col_name.to_s == primary_key
|
|
122
|
+
|
|
123
|
+
attr_sym = col_name.to_sym
|
|
124
|
+
masked_attribute_module.define_method(attr_sym) { read_attribute(attr_sym) { |name| missing_attribute(name, caller) } }
|
|
125
|
+
masked_attribute_module.define_method("#{attr_sym}=") do |value|
|
|
126
|
+
next super(value) unless pf_skip_masked_write?(attr_sym, value)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
prepend(masked_attribute_module)
|
|
130
|
+
|
|
131
|
+
const_set(:MaskedAttributeReaders, masked_attribute_module)
|
|
132
|
+
# A subclass that is already loaded may have generated column methods
|
|
133
|
+
# of its own, which it would keep calling: they are removed, so that
|
|
134
|
+
# it finds this class's, as a subclass loaded later does.
|
|
135
|
+
descendants.each do |klass|
|
|
136
|
+
PermissionForge::Concerns::MaskedAttributes.drop_generated_accessors(klass, masked_attribute_module)
|
|
137
|
+
end
|
|
138
|
+
pf_mask_attribute_aliases
|
|
139
|
+
@_pf_masked_attributes_applied = true
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Whether MaskedAttributes has already been applied to a class. Used by the
|
|
143
|
+
# engine's auto-apply to avoid re-prepending the readers module.
|
|
144
|
+
def self.applied?(klass)
|
|
145
|
+
klass.instance_variable_defined?(:@_pf_masked_attributes_applied) &&
|
|
146
|
+
klass.instance_variable_get(:@_pf_masked_attributes_applied)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Applied to the class or to a class it inherits from: a subclass is
|
|
150
|
+
# masked through its parent (the concern is never included into it).
|
|
151
|
+
#
|
|
152
|
+
# @param klass [Class]
|
|
153
|
+
# @return [Boolean]
|
|
154
|
+
def self.applied_in_chain?(klass)
|
|
155
|
+
klass.ancestors.any? { |ancestor| ancestor.is_a?(Class) && applied?(ancestor) }
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Removes from the module Rails generated a subclass's attribute methods
|
|
159
|
+
# into the ones `readers` covers. Rails gives a subclass column methods of
|
|
160
|
+
# its own unless its parent defines them outside such a module, which the
|
|
161
|
+
# parent does from the moment it has the concern; a subclass used before
|
|
162
|
+
# that has them already. Under the lock Rails generates them under, so a
|
|
163
|
+
# generation in progress on another thread is finished first.
|
|
164
|
+
#
|
|
165
|
+
# A method Rails undefined there (reset_column_information) is not found
|
|
166
|
+
# by `remove_method`, and would stop the lookup short of the parent's:
|
|
167
|
+
# it is defined again first, as a call to `super`, then removed.
|
|
168
|
+
def self.drop_generated_accessors(klass, readers)
|
|
169
|
+
generated = klass.send(:generated_attribute_methods)
|
|
170
|
+
with_attribute_methods_lock(generated) do
|
|
171
|
+
readers.instance_methods(false).each do |name|
|
|
172
|
+
generated.define_method(name) { |*args| super(*args) } unless generated.method_defined?(name, false)
|
|
173
|
+
generated.send(:remove_method, name)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Rails 7.2 and later generate attribute methods under one lock; 7.1
|
|
179
|
+
# under one per generated module.
|
|
180
|
+
def self.with_attribute_methods_lock(generated, &)
|
|
181
|
+
owner = ActiveRecord::AttributeMethods::GeneratedAttributeMethods
|
|
182
|
+
owner.const_defined?(:LOCK) ? owner::LOCK.synchronize(&) : generated.synchronize(&)
|
|
183
|
+
end
|
|
184
|
+
private_class_method :with_attribute_methods_lock
|
|
185
|
+
|
|
186
|
+
# Diagnostic: explains exactly what the guard decided for a given record
|
|
187
|
+
# and attribute, and why. Run from the console when a field returns an
|
|
188
|
+
# unexpected value (nil where you expected raw, raw where you expected masked, etc).
|
|
189
|
+
#
|
|
190
|
+
# @example
|
|
191
|
+
# PermissionForge::Concerns::MaskedAttributes.explain(customer, :email)
|
|
192
|
+
# rubocop:disable-next Metrics/AbcSize
|
|
193
|
+
def self.explain(record, attr_name)
|
|
194
|
+
config = PermissionForge.configuration
|
|
195
|
+
user = PermissionForge.current_user
|
|
196
|
+
klass = record.class
|
|
197
|
+
chain = PermissionForge::ResourceChain.names(klass)
|
|
198
|
+
raw = record.read_attribute_before_type_cast(attr_name) || record[attr_name]
|
|
199
|
+
field_name = AliasAccess.target(klass, attr_name) || attr_name
|
|
200
|
+
field = PermissionForge::ResourceChain.field_permissions(user, chain, field_name)
|
|
201
|
+
|
|
202
|
+
{
|
|
203
|
+
attribute: attr_name,
|
|
204
|
+
class: klass.name,
|
|
205
|
+
resource_chain: chain,
|
|
206
|
+
masked_attributes_applied: applied_in_chain?(klass),
|
|
207
|
+
primary_key: klass.primary_key,
|
|
208
|
+
is_primary_key: klass.primary_key.to_s == attr_name.to_s,
|
|
209
|
+
current_user_present: !user.nil?,
|
|
210
|
+
masked_attributes_nil_user_strategy: config.masked_attributes_nil_user_strategy, user_model_exempt: config.user_model_class?(klass),
|
|
211
|
+
user_responds_to_field_permissions: user.respond_to?(:field_permissions),
|
|
212
|
+
resource_in_permissions_hash: entries?(user, chain),
|
|
213
|
+
field_configured: !field.nil?,
|
|
214
|
+
field_entry: field,
|
|
215
|
+
unconfigured_field_strategy: config.unconfigured_field_strategy,
|
|
216
|
+
omit_hidden_in_attributes_hash: config.omit_hidden_in_attributes_hash,
|
|
217
|
+
raw_value: raw,
|
|
218
|
+
returned_value: record.public_send(attr_name)
|
|
219
|
+
}
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Whether the user's hash has a resource entry along the chain.
|
|
223
|
+
def self.entries?(user, chain)
|
|
224
|
+
user.respond_to?(:permissions_hash) && PermissionForge::ResourceChain.entries(user, chain).any?
|
|
225
|
+
end
|
|
226
|
+
private_class_method :entries?
|
|
227
|
+
|
|
228
|
+
# Diagnostic: walks every column and virtual reader on the record and reports
|
|
229
|
+
# raw vs. returned, plus the decision the guard made (passed / masked / hidden /
|
|
230
|
+
# unconfigured / bypassed). A virtual reader that is not masked on the model
|
|
231
|
+
# (config.mask_virtual_attributes off for it) reports :virtual_masking_off.
|
|
232
|
+
# Prints a readable table to stdout and returns the rows for programmatic use.
|
|
233
|
+
#
|
|
234
|
+
# @example
|
|
235
|
+
# PermissionForge::Concerns::MaskedAttributes.explain_all(customer)
|
|
236
|
+
def self.explain_all(record)
|
|
237
|
+
klass = record.class
|
|
238
|
+
rows = klass.column_names.map { |col| explain_row(record, col, record[col]) } +
|
|
239
|
+
virtual_reader_names(record).map { |name| explain_virtual_row(record, name) }
|
|
240
|
+
print_explain_table(rows)
|
|
241
|
+
rows
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def self.explain_row(record, attr_name, raw)
|
|
245
|
+
klass = record.class
|
|
246
|
+
user = PermissionForge.current_user
|
|
247
|
+
field = PermissionForge::ResourceChain.field_permissions(user, PermissionForge::ResourceChain.names(klass), attr_name)
|
|
248
|
+
|
|
249
|
+
decision = decide(attr_name: attr_name, klass: klass, user: user, field: field)
|
|
250
|
+
|
|
251
|
+
{ attribute: attr_name, decision: decision, configured: !field.nil?,
|
|
252
|
+
read_access: field&.dig(:read_access), mask_access: field&.dig(:mask_access),
|
|
253
|
+
mask_strategy: field&.dig(:mask_strategy), raw: raw, returned: record.public_send(attr_name), virtual: false }
|
|
254
|
+
end
|
|
255
|
+
private_class_method :explain_row
|
|
256
|
+
|
|
257
|
+
# The model's virtual fields that have a reader, masked on the model or not.
|
|
258
|
+
def self.virtual_reader_names(record)
|
|
259
|
+
klass = record.class
|
|
260
|
+
klass.respond_to?(:pf_virtual_readers) ? klass.pf_virtual_readers : []
|
|
261
|
+
end
|
|
262
|
+
private_class_method :virtual_reader_names
|
|
263
|
+
|
|
264
|
+
def self.explain_virtual_row(record, name)
|
|
265
|
+
raw = record.pf_without_virtual_masking { record.public_send(name) }
|
|
266
|
+
row = explain_row(record, name, raw).merge(virtual: true)
|
|
267
|
+
VirtualReaders.masked?(record.class, name) ? row : row.merge(decision: :virtual_masking_off)
|
|
268
|
+
end
|
|
269
|
+
private_class_method :explain_virtual_row
|
|
270
|
+
|
|
271
|
+
def self.decide(attr_name:, klass:, user:, field:)
|
|
272
|
+
return :primary_key_skipped if attr_name.to_s == klass.primary_key.to_s
|
|
273
|
+
return without_user_decision(klass) if user.nil?
|
|
274
|
+
return :unconfigured if field.nil?
|
|
275
|
+
return :hidden_no_read_access unless field[:read_access]
|
|
276
|
+
return :masked if field[:mask_access]
|
|
277
|
+
|
|
278
|
+
:passed_raw
|
|
279
|
+
end
|
|
280
|
+
private_class_method :decide
|
|
281
|
+
|
|
282
|
+
# What the guard does with no user (config.masked_attributes_nil_user_strategy;
|
|
283
|
+
# the user model is exempt from :hide).
|
|
284
|
+
def self.without_user_decision(klass)
|
|
285
|
+
config = PermissionForge.configuration
|
|
286
|
+
return :no_user_bypassed unless config.hide_masked_attributes_without_user?
|
|
287
|
+
|
|
288
|
+
config.user_model_class?(klass) ? :no_user_user_model_exempt : :no_user_hidden
|
|
289
|
+
end
|
|
290
|
+
private_class_method :without_user_decision
|
|
291
|
+
|
|
292
|
+
def self.print_explain_table(rows)
|
|
293
|
+
return if rows.empty?
|
|
294
|
+
|
|
295
|
+
Rails.logger&.info('[PermissionForge.explain_all]')
|
|
296
|
+
rows.each do |r|
|
|
297
|
+
line = format(
|
|
298
|
+
'%<attribute>-22s decision=%<decision>-22s configured=%<configured>-5s raw=%<raw>s -> returned=%<returned>s',
|
|
299
|
+
attribute: r[:attribute], decision: r[:decision], configured: r[:configured],
|
|
300
|
+
raw: r[:raw].inspect, returned: r[:returned].inspect
|
|
301
|
+
)
|
|
302
|
+
puts line
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
private_class_method :print_explain_table
|
|
306
|
+
|
|
307
|
+
# Reads an attribute through the field permissions of the current user.
|
|
308
|
+
# @param attr_name [String, Symbol]
|
|
309
|
+
def read_attribute(attr_name, &)
|
|
310
|
+
name = attr_name.to_s
|
|
311
|
+
raw = super(name, &)
|
|
312
|
+
# An alias is judged as the attribute it points to (AliasAccess); what
|
|
313
|
+
# is read is what Rails reads for it.
|
|
314
|
+
target = self.class.attribute_aliases[name]
|
|
315
|
+
attr_name = name = pf_alias_end(target) if target
|
|
316
|
+
return raw if pf_read_as_stored?(name)
|
|
317
|
+
|
|
318
|
+
include_field, value = pf_guard(attr_name, raw)
|
|
319
|
+
value = PermissionForge.configuration.hidden_attribute_value unless include_field
|
|
320
|
+
# A returned value that differs from the raw column value was masked or
|
|
321
|
+
# hidden; remember the exact sentinel so a write-back of it can be refused.
|
|
322
|
+
pf_record_masked_read(name, value) unless include_field && value == raw
|
|
323
|
+
value
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# The user is resolved once for the whole hash. With no user every
|
|
327
|
+
# value is returned as stored (or hidden, under
|
|
328
|
+
# config.masked_attributes_nil_user_strategy = :hide); the decisions
|
|
329
|
+
# (bypassed) are still published, as for a getter or as_json, but only
|
|
330
|
+
# when someone listens.
|
|
331
|
+
def attributes
|
|
332
|
+
raw = super
|
|
333
|
+
user = pf_guarded_user
|
|
334
|
+
return raw unless user || pf_hide_without_user? || PermissionForge::Audit::FieldAccess::GATE.open?
|
|
335
|
+
|
|
336
|
+
PermissionForge::Audit::FieldAccess.read(self, self.class.name, record: self) do
|
|
337
|
+
pf_guard_attributes_hash(raw, user)
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# as_json / to_json / serializable_hash: one read of the record, so one
|
|
342
|
+
# field.access event for all the fields it serializes.
|
|
343
|
+
def serializable_hash(options = nil)
|
|
344
|
+
PermissionForge::Audit::FieldAccess.read(self, self.class.name, record: self) { super }
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# Hook used by ActiveModel::Serialization (and therefore as_json / to_json /
|
|
348
|
+
# serializable_hash). Filtering the *names* here means hidden keys never get
|
|
349
|
+
# serialized at all — matching Salesforce stripInaccessible semantics.
|
|
350
|
+
def attribute_names_for_serialization
|
|
351
|
+
names = super
|
|
352
|
+
user = pf_guarded_user
|
|
353
|
+
return names unless user || pf_hide_without_user?
|
|
354
|
+
return names unless PermissionForge.configuration.omit_hidden_in_attributes_hash
|
|
355
|
+
|
|
356
|
+
hidden, shown = names.partition { |name| pf_hidden_attribute?(name, user) }
|
|
357
|
+
PermissionForge::Audit::FieldAccess.omitted(self, self.class.pf_resource_symbol, hidden, user) unless hidden.empty?
|
|
358
|
+
shown
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# AR's default routes through `send(name)` which hits the AR attribute fast
|
|
362
|
+
# path and bypasses our `read_attribute` override. Routing real columns
|
|
363
|
+
# through `read_attribute` lets the masking logic apply during serialization.
|
|
364
|
+
# Virtual attributes (defined as methods) still go through the original path.
|
|
365
|
+
# A column that was not loaded raises, as that original path does.
|
|
366
|
+
def read_attribute_for_serialization(name)
|
|
367
|
+
if self.class.attribute_names.include?(name.to_s)
|
|
368
|
+
read_attribute(name) { |missing| missing_attribute(missing, caller) }
|
|
369
|
+
else
|
|
370
|
+
super
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# What inspect, pretty_print and the console print for an attribute: the
|
|
375
|
+
# value the current user's getter returns, formatted as Rails formats it
|
|
376
|
+
# (filter_attributes still apply on top). Rails reads the raw attribute
|
|
377
|
+
# here, so a record in a log line or an error report used to show masked
|
|
378
|
+
# and hidden values in full. Not a field read: no field.access decision,
|
|
379
|
+
# no hook. And never an error: inspect runs inside error reporters and
|
|
380
|
+
# rescue blocks, often in the failed transaction itself, so a read that
|
|
381
|
+
# fails — or a Rails formatting of the value that fails (a private
|
|
382
|
+
# method, renamed some day) — shows the hidden value, never the stored
|
|
383
|
+
# one.
|
|
384
|
+
def attribute_for_inspect(attr_name)
|
|
385
|
+
name = attr_name.to_s
|
|
386
|
+
name = self.class.attribute_aliases[name] || name
|
|
387
|
+
pf_while_inspecting { format_for_inspect(name, read_attribute(name)) }
|
|
388
|
+
rescue StandardError
|
|
389
|
+
pf_hidden_for_inspect
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
# Rails 7.1 reads the raw attribute in pretty_print; 7.2 and later print
|
|
393
|
+
# attribute_for_inspect. The 7.2 listing, for 7.1.
|
|
394
|
+
if ActiveRecord.gem_version < Gem::Version.new('7.2')
|
|
395
|
+
# Prints the record without revealing masked or hidden values.
|
|
396
|
+
def pretty_print(pp)
|
|
397
|
+
return super if custom_inspect_method_defined?
|
|
398
|
+
|
|
399
|
+
pp.object_address_group(self) do
|
|
400
|
+
next pf_pp_not_initialized(pp) unless defined?(@attributes) && @attributes
|
|
401
|
+
|
|
402
|
+
attr_names = self.class.attribute_names.select { |name| _has_attribute?(name) }
|
|
403
|
+
pp.seplist(attr_names, proc { pp.text ',' }) { |attr_name| pf_pp_attribute(pp, attr_name) }
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def pf_pp_attribute(pp, attr_name)
|
|
408
|
+
pp.breakable ' '
|
|
409
|
+
pp.group(1) do
|
|
410
|
+
pp.text attr_name
|
|
411
|
+
pp.text ':'
|
|
412
|
+
pp.breakable
|
|
413
|
+
pp.text attribute_for_inspect(attr_name)
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def pf_pp_not_initialized(pp)
|
|
418
|
+
pp.breakable ' '
|
|
419
|
+
pp.text 'not initialized'
|
|
420
|
+
end
|
|
421
|
+
private :pf_pp_attribute, :pf_pp_not_initialized
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
private
|
|
425
|
+
|
|
426
|
+
# Whether the user's entry for `name`, along this class's resource
|
|
427
|
+
# chain, denies read. False for an unconfigured field. With no user:
|
|
428
|
+
# false, or true for everything but the primary key under :hide.
|
|
429
|
+
def pf_hidden_attribute?(name, user = pf_guarded_user)
|
|
430
|
+
return pf_hide_without_user? && name.to_s != self.class.primary_key unless user
|
|
431
|
+
|
|
432
|
+
field = PermissionForge::ResourceChain.field_permissions(user, self.class.pf_resource_chain, name)
|
|
433
|
+
field ? !field[:read_access] : false
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
# The primary key is never masked; a store column is read as stored
|
|
437
|
+
# while a store accessor writes one of its keys (StoreAccess), and the
|
|
438
|
+
# column a validator reads while it reads it (ValidationReads).
|
|
439
|
+
def pf_read_as_stored?(name)
|
|
440
|
+
name == self.class.primary_key || pf_raw_store_column?(name) || name == pf_validation_read
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
# config.masked_attributes_nil_user_strategy = :hide — with no user
|
|
444
|
+
# every guarded attribute is hidden (AttributeAccessGuard#without_user),
|
|
445
|
+
# except on the user model, which sign-in reads before anyone is
|
|
446
|
+
# signed in.
|
|
447
|
+
def pf_hide_without_user?
|
|
448
|
+
PermissionForge.configuration.hide_masked_attributes_without_user_for?(self.class)
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
# Every guarded read of this record is one field.access read
|
|
452
|
+
# (Audit::FieldAccess), joined by the reads made inside it. Nobody
|
|
453
|
+
# listening (the gate is shut): one check, nothing collected.
|
|
454
|
+
def pf_guard(attr_name, raw, user = pf_guarded_user)
|
|
455
|
+
chain = self.class.pf_resource_chain
|
|
456
|
+
unless PermissionForge::Audit::FieldAccess::GATE.open? && !PermissionForge::ExecutionState[INSPECT_KEY]
|
|
457
|
+
return PermissionForge::Services::AttributeAccessGuard.call(
|
|
458
|
+
resource_name: chain, attribute: attr_name, raw_value: raw, user: user, observer: nil, model: self.class
|
|
459
|
+
)
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
PermissionForge::Audit::FieldAccess.read(self, self.class.name, record: self) do |frame|
|
|
463
|
+
PermissionForge::Services::AttributeAccessGuard.call(
|
|
464
|
+
resource_name: chain, attribute: attr_name, raw_value: raw, user: user, observer: frame, model: self.class
|
|
465
|
+
)
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
# Marks the reads in the block as debugging reads: no field.access
|
|
470
|
+
# decision, no hook.
|
|
471
|
+
def pf_while_inspecting(&)
|
|
472
|
+
PermissionForge::ExecutionState.with(INSPECT_KEY, true, &)
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
# What inspect prints for an attribute whose read or formatting failed:
|
|
476
|
+
# the hidden value as inspect prints it — a String whatever the
|
|
477
|
+
# configured value is, so inspect itself never raises.
|
|
478
|
+
def pf_hidden_for_inspect
|
|
479
|
+
PermissionForge.configuration.hidden_attribute_value.inspect
|
|
480
|
+
rescue StandardError
|
|
481
|
+
'nil'
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
# Per-instance record of the exact masked/hidden sentinel handed out for
|
|
485
|
+
# each attribute, keyed by attribute name string.
|
|
486
|
+
def pf_masked_reads
|
|
487
|
+
@pf_masked_reads ||= {}
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
def pf_record_masked_read(attr_name, sentinel)
|
|
491
|
+
pf_masked_reads[attr_name.to_s] = sentinel
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
# True when the guard should suppress this write because `value` is a masked/
|
|
495
|
+
# hidden sentinel being written back over real data. Honors
|
|
496
|
+
# config.masked_write_protection. Two detections:
|
|
497
|
+
# 1. Ledger — this instance handed out that exact sentinel earlier (covers
|
|
498
|
+
# same-instance read-modify-write, e.g. a before_save callback).
|
|
499
|
+
# 2. Stateless — `value` equals what the mask would produce for the value
|
|
500
|
+
# CURRENTLY persisted in the DB. This survives a request boundary: a form
|
|
501
|
+
# rendered "********" and submitted back onto a freshly-loaded record has
|
|
502
|
+
# no ledger entry, but still matches the persisted value's mask.
|
|
503
|
+
def pf_skip_masked_write?(attr_name, value)
|
|
504
|
+
protection = PermissionForge.configuration.masked_write_protection
|
|
505
|
+
return false if protection == :off
|
|
506
|
+
|
|
507
|
+
ledger_hit = pf_masked_reads.key?(attr_name.to_s) && pf_masked_reads[attr_name.to_s] == value
|
|
508
|
+
return false unless ledger_hit || pf_matches_persisted_sentinel?(attr_name, value)
|
|
509
|
+
|
|
510
|
+
pf_masked_write_refused(attr_name, protection, ledger_hit)
|
|
511
|
+
raise MaskedWriteError, attr_name if protection == :raise
|
|
512
|
+
|
|
513
|
+
true # :skip — preserve the stored value by not calling super
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
# write.masked_skipped: which attribute, how it was refused (:skip
|
|
517
|
+
# keeps the stored value, :raise raised) and how the sentinel was
|
|
518
|
+
# recognised. Never the value.
|
|
519
|
+
def pf_masked_write_refused(attr_name, protection, ledger_hit)
|
|
520
|
+
event = 'write.masked_skipped'
|
|
521
|
+
return unless PermissionForge::Audit.instrument?(event)
|
|
522
|
+
|
|
523
|
+
user = PermissionForge.current_user
|
|
524
|
+
PermissionForge::Audit.instrument(event) do
|
|
525
|
+
{ record: self, subject_user_id: PermissionForge::Audit::Runtime.user_id(user),
|
|
526
|
+
data: { 'model' => self.class.name, 'attribute' => attr_name.to_s, 'protection' => protection.to_s,
|
|
527
|
+
'detected_by' => ledger_hit ? 'ledger' : 'persisted_value' },
|
|
528
|
+
objects: { user: user, record: self } }
|
|
529
|
+
end
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
# True when `value` equals the masked/hidden sentinel derived from the value
|
|
533
|
+
# currently persisted in the database for this attribute, and differs from
|
|
534
|
+
# that persisted value (writing the same value is a harmless no-op either way).
|
|
535
|
+
def pf_matches_persisted_sentinel?(attr_name, value)
|
|
536
|
+
field = pf_current_field_permissions(attr_name)
|
|
537
|
+
return false unless field
|
|
538
|
+
return value == PermissionForge.configuration.hidden_attribute_value unless field[:read_access]
|
|
539
|
+
return false unless field[:mask_access]
|
|
540
|
+
|
|
541
|
+
pf_persisted_mask_matches?(attr_name, value, field[:mask_strategy])
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
def pf_current_field_permissions(attr_name)
|
|
545
|
+
PermissionForge::ResourceChain.field_permissions(PermissionForge.current_user, self.class.pf_resource_chain, attr_name)
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
def pf_persisted_mask_matches?(attr_name, value, strategy)
|
|
549
|
+
persisted = attribute_in_database(attr_name.to_s)
|
|
550
|
+
sentinel = PermissionForge::Services::MaskValue.call(persisted, strategy: strategy || 'full')
|
|
551
|
+
value == sentinel && value != persisted
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
def pf_guard_attributes_hash(raw, user)
|
|
555
|
+
omit_hidden = PermissionForge.configuration.omit_hidden_in_attributes_hash
|
|
556
|
+
hidden_value = PermissionForge.configuration.hidden_attribute_value
|
|
557
|
+
|
|
558
|
+
primary_key = self.class.primary_key.to_s
|
|
559
|
+
raw.filter_map do |key, value|
|
|
560
|
+
# The primary key is never guarded (read_attribute and as_json keep it).
|
|
561
|
+
next [key, value] if key.to_s == primary_key
|
|
562
|
+
|
|
563
|
+
include_field, masked = pf_guard(key, value, user)
|
|
564
|
+
next [key, masked] if include_field
|
|
565
|
+
next if omit_hidden
|
|
566
|
+
|
|
567
|
+
[key, hidden_value]
|
|
568
|
+
end.to_h
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
def pf_guarded_user
|
|
572
|
+
PermissionForge.current_user
|
|
573
|
+
end
|
|
574
|
+
end
|
|
575
|
+
end
|
|
576
|
+
end
|