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,416 @@
|
|
|
1
|
+
module PermissionForge
|
|
2
|
+
module Concerns
|
|
3
|
+
# Cache read/write for user permissions.
|
|
4
|
+
# Provides the shared hash navigation helpers used by ResourcePermissionChecker and FieldMaskChecker.
|
|
5
|
+
#
|
|
6
|
+
# Reads are read-through: a cache miss (first access, TTL expiry, eviction,
|
|
7
|
+
# store flush) transparently rebuilds the hash in the *current* request
|
|
8
|
+
# context, so permission checks never silently operate on an empty hash.
|
|
9
|
+
# Cross-user changes (an admin editing permission records) invalidate the
|
|
10
|
+
# affected users' entries instead of rebuilding them — the rebuild happens
|
|
11
|
+
# lazily in each user's own request.
|
|
12
|
+
#
|
|
13
|
+
# A permission set with activation conditions (session flag, IP
|
|
14
|
+
# allowlist, login hours) counts for the request that READS the hash,
|
|
15
|
+
# never for the one that happened to build it. The entry under the
|
|
16
|
+
# user's key — the base — is built from the unconditional sets and
|
|
17
|
+
# carries the conditional ones with their conditions
|
|
18
|
+
# (PermissionForge::ConditionalSets). Every read evaluates the request
|
|
19
|
+
# context (config.request_context_resolver) against them: when none
|
|
20
|
+
# passes the base is the hash; when some pass, the hash is a variant
|
|
21
|
+
# built from the unconditional sets plus those, under a key derived from
|
|
22
|
+
# the base entry — read from the store, or built read-through and
|
|
23
|
+
# verified against the same invalidation stamp as the base
|
|
24
|
+
# (PermissionsPerContext). A user without conditional sets pays nothing
|
|
25
|
+
# for this: one store read, no resolver call, as before. A resolver that
|
|
26
|
+
# raises (one that reads the request where there is none) raises on
|
|
27
|
+
# every read of a user with conditional sets.
|
|
28
|
+
#
|
|
29
|
+
# A rebuild can race an invalidation: it reads the permission rows, an
|
|
30
|
+
# admin commits and invalidates, and only then does the rebuild store what
|
|
31
|
+
# it read. Each invalidation therefore leaves a per-user stamp in the
|
|
32
|
+
# cache (Cache::Invalidation), and a rebuild only leaves its hash in the
|
|
33
|
+
# cache when the stamp it saw before reading the rows is still there after
|
|
34
|
+
# storing it (Cache::Rebuild). A cache hit never looks at the stamp.
|
|
35
|
+
#
|
|
36
|
+
# Two per-request caches would defeat that check, so a rebuild goes past
|
|
37
|
+
# both: it reads the permission rows outside ActiveRecord's query cache
|
|
38
|
+
# (the repeat would otherwise be answered with the rows of the first
|
|
39
|
+
# attempt) and the stamp outside the cache store's local copy (the second
|
|
40
|
+
# read would otherwise be answered with the first).
|
|
41
|
+
#
|
|
42
|
+
# Inside a database transaction that reads from a snapshot
|
|
43
|
+
# (config.snapshot_transactions) a hash is not shared at all, and the
|
|
44
|
+
# store is not read again by the instance that built it (see
|
|
45
|
+
# regenerate_permissions and PermissionsMemo).
|
|
46
|
+
#
|
|
47
|
+
# Whatever happens to the store, the request that asked gets the hash that
|
|
48
|
+
# was built for it. An empty hash is never a safe substitute: it denies
|
|
49
|
+
# every can_*? check but also empties permission_set_rules, which switches
|
|
50
|
+
# row-level rules off.
|
|
51
|
+
#
|
|
52
|
+
# @api public
|
|
53
|
+
module PermissionCacheAccessor
|
|
54
|
+
extend ActiveSupport::Concern
|
|
55
|
+
include PermissionsMemo
|
|
56
|
+
include PermissionsPerContext
|
|
57
|
+
|
|
58
|
+
# Returns the cached permission hash for this user, rebuilding it on a
|
|
59
|
+
# cache miss (read-through).
|
|
60
|
+
#
|
|
61
|
+
# Memoized on the instance: reading from the cache store deserializes the
|
|
62
|
+
# entire hash (for broad grants, a multi-megabyte object graph), and hot
|
|
63
|
+
# paths — MaskedAttributes getters, response filtering, per-field checks —
|
|
64
|
+
# call this method once per attribute. Without a memo each of those calls
|
|
65
|
+
# pays a full store round-trip + deserialize (observed: a 40s admin page).
|
|
66
|
+
# The memo is invalidated by (a) a different permission_cache_key (tenant
|
|
67
|
+
# switch), (b) any bump of PermissionForge.permissions_cache_version —
|
|
68
|
+
# which invalidate_permissions, regenerate_permissions, and cache-store
|
|
69
|
+
# swaps all perform — so no instance in this process can outlive an
|
|
70
|
+
# invalidation with stale data, (c) for a hash that was withheld
|
|
71
|
+
# from the cache inside a transaction, the end of that transaction (or
|
|
72
|
+
# its rollback to a savepoint), and (d) for a user with conditional
|
|
73
|
+
# permission sets, a request context that selects other conditional
|
|
74
|
+
# sets than the memoized hash was built for. #reload also drops it,
|
|
75
|
+
# matching AR's association-cache semantics. See PermissionsMemo.
|
|
76
|
+
#
|
|
77
|
+
# Callers must treat the returned hash as read-only; every call on the
|
|
78
|
+
# same instance returns the same object. Deep-dup before mutating.
|
|
79
|
+
#
|
|
80
|
+
# @return [Hash] the full permission hash including :profile, :preferences, and :permissions
|
|
81
|
+
# @return [Hash] empty hash only when called re-entrantly during its own
|
|
82
|
+
# rebuild (or when a host override of regenerate_permissions never
|
|
83
|
+
# reaches the gem's build)
|
|
84
|
+
# @raise [PermissionForge::ConfigurationError] when the request context
|
|
85
|
+
# resolver reads this user's permissions (PermissionsPerContext)
|
|
86
|
+
def permissions_hash
|
|
87
|
+
key = permission_cache_key
|
|
88
|
+
memo = pf_memoized_permissions(key)
|
|
89
|
+
hash = pf_memoized_permissions_for_context(key, memo) if memo
|
|
90
|
+
return hash if hash
|
|
91
|
+
|
|
92
|
+
version = PermissionForge.permissions_cache_version
|
|
93
|
+
selected = pf_shared_or_rebuilt_permissions(key)
|
|
94
|
+
# asked by the build itself (see pf_rebuild_permissions_read_through): {} for that call alone
|
|
95
|
+
return selected.permissions if selected.rebuilt == :reentrant
|
|
96
|
+
|
|
97
|
+
pf_memoize_selected_permissions(key, version, selected)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Rebuilds and caches the permission hash from all active permission sets:
|
|
101
|
+
# the base entry, and — when the request context selects conditional
|
|
102
|
+
# sets — the variant for them. Returns the hash that applies to the
|
|
103
|
+
# context.
|
|
104
|
+
#
|
|
105
|
+
# The shared cache never keeps a hash that may be older than the latest
|
|
106
|
+
# invalidation (Cache::Rebuild): the entry is checked against the user's
|
|
107
|
+
# invalidation stamp, built once more when an invalidation overtook it,
|
|
108
|
+
# and removed when it cannot be verified.
|
|
109
|
+
#
|
|
110
|
+
# Inside a database transaction that reads from a snapshot
|
|
111
|
+
# (PermissionForge::OpenTransaction.withholding, decided by
|
|
112
|
+
# config.snapshot_transactions) the store is not touched at all. Under
|
|
113
|
+
# REPEATABLE READ or SERIALIZABLE — MySQL's default, and what a SQLite
|
|
114
|
+
# transaction gives — the rows a rebuild reads come from a snapshot
|
|
115
|
+
# that may predate an invalidation that has already finished, so the
|
|
116
|
+
# stamp check would pass on a stale hash. The hash stays on this
|
|
117
|
+
# instance, which does not read the store again until that transaction
|
|
118
|
+
# has ended; another instance of the user goes on reading the entry of
|
|
119
|
+
# the committed rows.
|
|
120
|
+
#
|
|
121
|
+
# Asking for a rebuild says that something changed, though (the way to
|
|
122
|
+
# refresh after update_all, insert_all or delete_all, which run no
|
|
123
|
+
# callbacks). Called inside such a transaction, the user is therefore
|
|
124
|
+
# invalidated once the outermost transaction has committed
|
|
125
|
+
# (Cache::Settlement) — whatever another request cached from the rows as
|
|
126
|
+
# they were before the commit goes with it. A read-through rebuild
|
|
127
|
+
# (a cache miss in permissions_hash) changed nothing: it arranges
|
|
128
|
+
# nothing and retires nobody else's memo.
|
|
129
|
+
#
|
|
130
|
+
# In any other transaction (PostgreSQL at READ COMMITTED) a rebuild
|
|
131
|
+
# reads committed rows plus the transaction's own, and is stored like
|
|
132
|
+
# one outside a transaction — also when the transaction then rolls
|
|
133
|
+
# back: invalidate after a rollback.
|
|
134
|
+
#
|
|
135
|
+
# In every case this instance keeps what it built, for the request that
|
|
136
|
+
# asked: a snapshot a moment old, like any request that finished just
|
|
137
|
+
# before the commit.
|
|
138
|
+
#
|
|
139
|
+
# @return [Hash] the hash that applies to the current request context
|
|
140
|
+
def regenerate_permissions
|
|
141
|
+
PermissionForge::Services::HostKeyGuard.verify!
|
|
142
|
+
|
|
143
|
+
key = permission_cache_key
|
|
144
|
+
transaction = PermissionForge::OpenTransaction.withholding
|
|
145
|
+
reading_through = pf_read_through_build(key)
|
|
146
|
+
pf_settle_permissions_after_commit(nil, transaction) unless reading_through
|
|
147
|
+
|
|
148
|
+
selected = pf_rebuild_permissions(key, transaction)
|
|
149
|
+
reading_through[:rebuilt] = selected if reading_through
|
|
150
|
+
|
|
151
|
+
pf_clear_permissions_memo
|
|
152
|
+
PermissionForge.bump_permissions_cache_version! unless transaction && reading_through
|
|
153
|
+
pf_memoize_permissions(selected.memo(key, PermissionForge.permissions_cache_version))
|
|
154
|
+
# a read-through rebuild is reported by permissions_hash, once memoized
|
|
155
|
+
pf_report_rebuilt(transaction, read_through: false) unless reading_through
|
|
156
|
+
selected.permissions
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Removes this user's cached permission hash so the next read rebuilds it
|
|
160
|
+
# (read-through) in that request's own context. Removes the ambient key,
|
|
161
|
+
# the tenant-agnostic key, and — when a tenant id is supplied by the
|
|
162
|
+
# caller (e.g., a callback on a tenant-scoped record) — that tenant's key,
|
|
163
|
+
# so background-job invalidations can't desynchronize from web requests;
|
|
164
|
+
# and the keys of the previous entry namespace (Cache::UserKeys).
|
|
165
|
+
# Nothing is read. The per-context variants of a user with conditional
|
|
166
|
+
# sets stay until their TTL: no read can reach them once the base entry
|
|
167
|
+
# that carries their token is gone (ConditionalSets); erasure and
|
|
168
|
+
# cache:clear remove them (Cache::VariantRemoval).
|
|
169
|
+
#
|
|
170
|
+
# A fresh invalidation stamp is written before the keys are removed, one
|
|
171
|
+
# per user whatever the tenant (an invalidation in another tenant makes a
|
|
172
|
+
# rebuild repeat itself, which costs time, not correctness). The store's
|
|
173
|
+
# answers are checked, an unconfirmed invalidation is retried, and one
|
|
174
|
+
# that still fails is reported through Cache::Failure — it does not
|
|
175
|
+
# raise (see Cache::Invalidation).
|
|
176
|
+
#
|
|
177
|
+
# Inside a database transaction that reads from a snapshot
|
|
178
|
+
# (config.snapshot_transactions) the invalidation is made at once and
|
|
179
|
+
# once more when the outermost transaction has committed
|
|
180
|
+
# (Cache::Settlement): between the two, another request can cache the
|
|
181
|
+
# rows as they still are before the commit. This instance does not read
|
|
182
|
+
# the store again until then.
|
|
183
|
+
#
|
|
184
|
+
# @param tenant_id [Object, nil] tenant id of the record that changed, if known
|
|
185
|
+
# @return [Boolean] whether the store confirmed the invalidation
|
|
186
|
+
def invalidate_permissions(tenant_id: nil)
|
|
187
|
+
# the keys and stamp of Cache::UserKeys — shared with the invalidation of many holders by id
|
|
188
|
+
invalidation = PermissionForge::Cache::UserKeys.invalidation(id, tenant_id: tenant_id, ambient_key: permission_cache_key,
|
|
189
|
+
stamp_key: pf_invalidation_stamp_key)
|
|
190
|
+
confirmed = PermissionForge::Cache::Invalidation.call(**invalidation)
|
|
191
|
+
ensure
|
|
192
|
+
pf_clear_permissions_memo
|
|
193
|
+
PermissionForge.bump_permissions_cache_version!
|
|
194
|
+
# after the invalidation, whatever became of it: the repeat is what counts
|
|
195
|
+
pf_settle_permissions_after_commit(tenant_id)
|
|
196
|
+
# published once the memo and the version are gone: a subscriber that
|
|
197
|
+
# reads permissions_hash gets the state after the invalidation
|
|
198
|
+
PermissionForge::Audit::Runtime.invalidated(self, tenant_id: tenant_id, confirmed: confirmed == true)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# What the last invalidate_permissions of this user left in the cache
|
|
202
|
+
# (nil when there was none, or it expired), read from the store itself.
|
|
203
|
+
#
|
|
204
|
+
# For a host app that overrides regenerate_permissions to derive a cache
|
|
205
|
+
# entry of its own after `super`: that write races an invalidation the
|
|
206
|
+
# same way. Read the stamp before `super` and again after writing the
|
|
207
|
+
# entry, and delete the entry when the two differ.
|
|
208
|
+
#
|
|
209
|
+
# It lives as long as a hash may: a hash stored without noticing an
|
|
210
|
+
# invalidation would need a rebuild that took longer than cache_ttl, and
|
|
211
|
+
# would expire as soon.
|
|
212
|
+
#
|
|
213
|
+
# @return [String, nil]
|
|
214
|
+
def permission_invalidation_stamp
|
|
215
|
+
PermissionForge.cache.read_fresh(pf_invalidation_stamp_key)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# The key of this user's base entry in the ambient tenant (or none):
|
|
219
|
+
# "v2:tenant_<tenant id>_user_<id>" / "v2:user_<id>" (Cache::UserKeys).
|
|
220
|
+
#
|
|
221
|
+
# @return [String]
|
|
222
|
+
def permission_cache_key
|
|
223
|
+
PermissionForge::Cache::UserKeys.hash_key(id)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
private
|
|
227
|
+
|
|
228
|
+
# The hash for the current request context: from the store, or rebuilt
|
|
229
|
+
# on a miss (and never from the store while this instance withholds).
|
|
230
|
+
#
|
|
231
|
+
# @return [PermissionsPerContext::Selected]
|
|
232
|
+
def pf_shared_or_rebuilt_permissions(key)
|
|
233
|
+
entry = pf_read_shared_permissions(key) unless pf_withholding_permissions?
|
|
234
|
+
(entry && pf_select_shared_permissions(key, entry)) || pf_rebuild_permissions_read_through
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Memoizes what a read selected, and reports a rebuild. A hash read
|
|
238
|
+
# from the store — or a variant built for a base entry read from it —
|
|
239
|
+
# is memoized under `version`, the one from before the read, so an
|
|
240
|
+
# invalidation during the read retires it. A base rebuilt by
|
|
241
|
+
# regenerate_permissions under the version as it is now: a rebuild
|
|
242
|
+
# that was stored bumps it, which keeps the memo valid until the NEXT
|
|
243
|
+
# invalidation.
|
|
244
|
+
#
|
|
245
|
+
# @return [Hash] the hash
|
|
246
|
+
def pf_memoize_selected_permissions(key, version, selected)
|
|
247
|
+
rebuilt = selected.rebuilt
|
|
248
|
+
version = PermissionForge.permissions_cache_version if rebuilt == :base
|
|
249
|
+
pf_memoize_permissions(selected.memo(key, version))
|
|
250
|
+
# after the memo: building the event finds the hash, not a rebuild
|
|
251
|
+
pf_report_rebuilt(selected.transaction, read_through: true) if rebuilt
|
|
252
|
+
selected.permissions
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# permissions.rebuilt (Audit::Runtime): the hash was built, read-through
|
|
256
|
+
# or on request, inside a withholding transaction or not.
|
|
257
|
+
def pf_report_rebuilt(transaction, read_through:)
|
|
258
|
+
PermissionForge::Audit::Runtime.rebuilt(self, read_through: read_through, withheld: !transaction.nil?)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# The cached hash — unless this process still owes the store an
|
|
262
|
+
# invalidation of this user (Cache::Backlog). The entry may then be the
|
|
263
|
+
# very one that should be gone, so it is not read: the hash is rebuilt.
|
|
264
|
+
def pf_read_shared_permissions(key)
|
|
265
|
+
return if PermissionForge::Cache::Invalidation.owed?(pf_invalidation_stamp_key)
|
|
266
|
+
|
|
267
|
+
PermissionForge.cache.read(key)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Inside a transaction whose hashes are withheld: has the user
|
|
271
|
+
# invalidated when it has committed — for the tenant the caller named
|
|
272
|
+
# or the one that is ambient now (the commit may run after the block
|
|
273
|
+
# that made it ambient) — and keeps this instance away from the store
|
|
274
|
+
# until then.
|
|
275
|
+
def pf_settle_permissions_after_commit(tenant_id, transaction = PermissionForge::OpenTransaction.withholding)
|
|
276
|
+
return unless transaction
|
|
277
|
+
|
|
278
|
+
pf_withhold_permissions_in(transaction)
|
|
279
|
+
tenant_id ||= PermissionForge.ambient_tenant_id
|
|
280
|
+
PermissionForge::Cache::Settlement.for(transaction).add([:user, id, tenant_id]) do
|
|
281
|
+
invalidate_permissions(tenant_id: tenant_id)
|
|
282
|
+
rescue StandardError => e
|
|
283
|
+
PermissionForge::Cache::Failure.report(operation: :invalidate, user_id: id, error: e)
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def pf_invalidation_stamp_key
|
|
288
|
+
PermissionForge::Cache::UserKeys.stamp_key(id)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Rebuilds the hash on a cache miss. Guarded against re-entrancy: building
|
|
292
|
+
# the hash may read attributes on this user (profile_builder, extra fields),
|
|
293
|
+
# and with MaskedAttributes applied those getters consult permissions_hash
|
|
294
|
+
# again — the guard breaks that cycle by returning {} inside the rebuild.
|
|
295
|
+
#
|
|
296
|
+
# The request gets the hash that was built and — when the shared cache
|
|
297
|
+
# kept it — verified, not what the store holds a moment later: another
|
|
298
|
+
# rebuild that raced an invalidation may have its hash there for the
|
|
299
|
+
# instant before it removes it.
|
|
300
|
+
#
|
|
301
|
+
# A call made while the hash for the same key is being built on this
|
|
302
|
+
# thread — by the build's own code: this instance, or another instance
|
|
303
|
+
# of the user whose build reads a masked attribute — is answered {}
|
|
304
|
+
# marked :reentrant, which permissions_hash returns to that call alone
|
|
305
|
+
# and never memoizes. A host override of regenerate_permissions that
|
|
306
|
+
# returns without building the hash for this key raises: {} would deny
|
|
307
|
+
# every check and switch row-level rules off on every call.
|
|
308
|
+
#
|
|
309
|
+
# What the build hands back to this method is kept beside it, on this
|
|
310
|
+
# thread, never on the instance: one user instance may be read on
|
|
311
|
+
# several threads at once (handed to threads of its request, or kept
|
|
312
|
+
# across requests), and their rebuilds would take or clear each other's
|
|
313
|
+
# result — the one that lost it would be left with {}, and keep it.
|
|
314
|
+
#
|
|
315
|
+
# @return [PermissionsPerContext::Selected]
|
|
316
|
+
# @raise [PermissionForge::ConfigurationError] when no hash was built
|
|
317
|
+
def pf_rebuild_permissions_read_through
|
|
318
|
+
key = permission_cache_key
|
|
319
|
+
outcome = pf_guarding_permission_build(key) do |build|
|
|
320
|
+
regenerate_permissions
|
|
321
|
+
# a host override that ran super on another thread or fiber: super memoized it
|
|
322
|
+
build[:rebuilt] || pf_memoized_permissions(key)&.then { |memo| PermissionsPerContext::Selected.of_memo(memo) } ||
|
|
323
|
+
raise(PermissionForge::ConfigurationError,
|
|
324
|
+
"regenerate_permissions built no permission hash for #{key}: a host override must call super, " \
|
|
325
|
+
'under the same tenant')
|
|
326
|
+
end
|
|
327
|
+
outcome == :reentrant ? PermissionsPerContext::REENTRANT : outcome
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# Runs a build of the hash for `key` on this thread (fiber), one at a
|
|
331
|
+
# time: a call made while one runs gets :reentrant instead.
|
|
332
|
+
#
|
|
333
|
+
# @yieldparam build [Hash] where regenerate_permissions leaves what it built
|
|
334
|
+
# @return [Object, :reentrant] what the block returns
|
|
335
|
+
def pf_guarding_permission_build(key)
|
|
336
|
+
building = PermissionForge::ExecutionState.fetch(BUILDING_KEY) { {} }
|
|
337
|
+
return :reentrant if building[key]
|
|
338
|
+
|
|
339
|
+
build = building[key] = { instance: self }
|
|
340
|
+
begin
|
|
341
|
+
yield build
|
|
342
|
+
ensure
|
|
343
|
+
building.delete(key)
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# The read-through build this instance is making on this thread (fiber)
|
|
348
|
+
# of the hash for `key`, the key regenerate_permissions builds for: an
|
|
349
|
+
# override that changed the tenant before super builds for another key,
|
|
350
|
+
# which fills no build (pf_rebuild_permissions_read_through then raises).
|
|
351
|
+
def pf_read_through_build(key)
|
|
352
|
+
build = PermissionForge::ExecutionState[BUILDING_KEY]&.[](key)
|
|
353
|
+
build if build && build[:instance].equal?(self)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# Effective TTL for this user's cached hash:
|
|
357
|
+
# - config.cache_ttl as the baseline (default 1 hour),
|
|
358
|
+
# - clamped to conditional_set_cache_ttl when any set has activation
|
|
359
|
+
# conditions (the backstop for the base entry and its variants),
|
|
360
|
+
# - clamped to the soonest assignment expiry so an expired assignment can
|
|
361
|
+
# never keep granting from cache beyond its expires_at.
|
|
362
|
+
def pf_permission_cache_ttl(permission_sets, soonest = pf_soonest_assignment_expiry)
|
|
363
|
+
config = PermissionForge.configuration
|
|
364
|
+
[config.cache_ttl, pf_conditional_set_ttl(config, permission_sets), pf_expiry_ttl(soonest)].compact.min
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def pf_conditional_set_ttl(config, permission_sets)
|
|
368
|
+
return nil unless config.conditional_set_cache_ttl && permission_sets.any?(&:conditional?)
|
|
369
|
+
|
|
370
|
+
config.conditional_set_cache_ttl
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# When the soonest of the user's active assignments ends (nil: none does).
|
|
374
|
+
#
|
|
375
|
+
# @return [Time, nil]
|
|
376
|
+
def pf_soonest_assignment_expiry
|
|
377
|
+
permission_set_users.active_assignments.where.not(expires_at: nil).minimum(:expires_at)
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# Seconds until `soonest`, so an expired assignment can never keep
|
|
381
|
+
# granting from cache beyond its expires_at.
|
|
382
|
+
def pf_expiry_ttl(soonest)
|
|
383
|
+
[(soonest - Time.current).ceil, 1].max if soonest
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# The active, non-expired sets of the user — every one of them: which
|
|
387
|
+
# of the conditional ones count is decided per read (ConditionalSets).
|
|
388
|
+
#
|
|
389
|
+
# @return [Array<PermissionForge::PermissionSet>]
|
|
390
|
+
def pf_active_non_expired_permission_sets
|
|
391
|
+
active_psu_ids = permission_set_users.active_assignments.select(:permission_set_id)
|
|
392
|
+
# Deterministic order so "first mask_strategy wins" in FieldPermissionMerger
|
|
393
|
+
# is stable across requests and DB adapters (was DB-insertion-order dependent).
|
|
394
|
+
PermissionForge::PermissionSet.active.where(id: active_psu_ids).order(:created_at, :id).to_a
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# The 0.13 name of pf_active_non_expired_permission_sets, kept for host
|
|
398
|
+
# code that calls it with `send` (a hash built by hand, as the cache
|
|
399
|
+
# would hold it). The gem never calls it, so a host method of this name
|
|
400
|
+
# changes nothing here. Drop in 1.0.
|
|
401
|
+
def active_non_expired_permission_sets = pf_active_non_expired_permission_sets
|
|
402
|
+
|
|
403
|
+
def pf_resource_name_permissions(resource)
|
|
404
|
+
resource_sym = resource.is_a?(Symbol) ? resource : resource.to_s.to_sym
|
|
405
|
+
permissions_hash.dig(:permissions, :resources, resource_sym)
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def pf_resource_permissions(resource)
|
|
409
|
+
entry = pf_resource_name_permissions(resource)
|
|
410
|
+
return if entry.blank?
|
|
411
|
+
|
|
412
|
+
entry[:permissions]
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
end
|