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,45 @@
|
|
|
1
|
+
module PermissionForge
|
|
2
|
+
module Concerns
|
|
3
|
+
# Field-level mask checks.
|
|
4
|
+
# Depends on PermissionCacheAccessor for pf_resource_name_permissions.
|
|
5
|
+
#
|
|
6
|
+
# @api public
|
|
7
|
+
module FieldMaskChecker
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
# @param resource [Symbol, String] resource name
|
|
11
|
+
# @param field_name [Symbol, String] field name
|
|
12
|
+
# @return [Boolean] true if the field should be masked
|
|
13
|
+
#
|
|
14
|
+
# Fail-closed for unconfigured fields: when the field has no entry in the
|
|
15
|
+
# permission hash, the answer follows `unconfigured_field_strategy` — a
|
|
16
|
+
# blocking or mask-all strategy reports the field as masked rather than
|
|
17
|
+
# defaulting to "not masked" (which would leak plaintext).
|
|
18
|
+
def masked_field?(resource, field_name)
|
|
19
|
+
field = field_permissions(resource, field_name)
|
|
20
|
+
return field[:mask_access] == true if field
|
|
21
|
+
|
|
22
|
+
%i[block mask_all].include?(PermissionForge.configuration.unconfigured_field_strategy)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @param resource [Symbol, String] resource name
|
|
26
|
+
# @param field_name [Symbol, String] field name
|
|
27
|
+
# @return [String] mask strategy (e.g., 'full', 'email', 'last4')
|
|
28
|
+
def mask_strategy_for(resource, field_name)
|
|
29
|
+
field = field_permissions(resource, field_name)
|
|
30
|
+
field&.dig(:mask_strategy) || 'full'
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @param resource [Symbol, String] resource name
|
|
34
|
+
# @param field_name [Symbol, String] field name
|
|
35
|
+
# @return [Hash, nil] field permission hash with :read_access, :edit_access, :mask_access, :mask_strategy
|
|
36
|
+
def field_permissions(resource, field_name)
|
|
37
|
+
entry = pf_resource_name_permissions(resource)
|
|
38
|
+
return if entry.blank?
|
|
39
|
+
|
|
40
|
+
field_name = field_name.to_sym
|
|
41
|
+
entry.dig(:fields, field_name)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
module PermissionForge
|
|
2
|
+
module Concerns
|
|
3
|
+
# @api private
|
|
4
|
+
module HasAuditTrail
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
# class_name is config.audit_user_class as the model loads — after the
|
|
9
|
+
# host's config/initializers, since the models are autoloaded by the
|
|
10
|
+
# application (app/models). The callbacks are always registered but
|
|
11
|
+
# no-op at runtime when audit_trail_enabled is false — so
|
|
12
|
+
# config.audit_trail_enabled = false is a real toggle, not a
|
|
13
|
+
# load-time-frozen one.
|
|
14
|
+
belongs_to :created_by,
|
|
15
|
+
class_name: PermissionForge.configuration.audit_user_class,
|
|
16
|
+
optional: true
|
|
17
|
+
|
|
18
|
+
belongs_to :updated_by,
|
|
19
|
+
class_name: PermissionForge.configuration.audit_user_class,
|
|
20
|
+
optional: true
|
|
21
|
+
|
|
22
|
+
before_create :set_created_by
|
|
23
|
+
before_update :set_updated_by
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
# created_by is kept when given (a clone, an import); updated_by starts
|
|
29
|
+
# as whoever created the row.
|
|
30
|
+
def set_created_by
|
|
31
|
+
return unless PermissionForge.configuration.audit_trail_enabled
|
|
32
|
+
|
|
33
|
+
acting = audit_trail_user_id
|
|
34
|
+
self.created_by_id ||= acting
|
|
35
|
+
self.updated_by_id ||= acting || created_by_id
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Moves only to someone known: an edit by nobody (a job, a Basic-auth
|
|
39
|
+
# login, an API client) keeps the previous editor instead of blanking it.
|
|
40
|
+
def set_updated_by
|
|
41
|
+
return unless PermissionForge.configuration.audit_trail_enabled
|
|
42
|
+
|
|
43
|
+
acting = audit_trail_user_id
|
|
44
|
+
self.updated_by_id = acting if acting
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# The id of the acting user: the audit actor (an explicit
|
|
48
|
+
# with_audit_actor block, the request's actor, else
|
|
49
|
+
# PermissionForge.current_user) when it was made from a record of
|
|
50
|
+
# config.audit_user_class — the class the two columns point at. An
|
|
51
|
+
# admin of another model, or an actor given as a Hash, stamps nothing:
|
|
52
|
+
# its id would name somebody else.
|
|
53
|
+
def audit_trail_user_id
|
|
54
|
+
actor = PermissionForge::Audit::Actor.current
|
|
55
|
+
actor.id if actor.user_of?(PermissionForge.configuration.audit_user_class.to_s.safe_constantize)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Concerns
|
|
5
|
+
# Provides `users_on_permission_set` required by {PermissionsRegenerator},
|
|
6
|
+
# and `user_ids_on_permission_set`, the ids of their users, plucked
|
|
7
|
+
# without loading a row (what the invalidation uses).
|
|
8
|
+
#
|
|
9
|
+
# Include this concern and declare the lookup strategy based on how the model
|
|
10
|
+
# relates to a permission set:
|
|
11
|
+
#
|
|
12
|
+
# permission_set_user_lookup :self # PermissionSet (has permission_set_users directly)
|
|
13
|
+
# permission_set_user_lookup :direct # Has permission_set_id column
|
|
14
|
+
# permission_set_user_lookup :via_resource # Has resource_id -> resource -> permission_set
|
|
15
|
+
#
|
|
16
|
+
# Inside after_commit the queries go through the commit's InvalidationBatch
|
|
17
|
+
# (InvalidatesInBatch), so the records of one Permission Set written in one
|
|
18
|
+
# transaction look its assignments up once. Anywhere else every call
|
|
19
|
+
# queries afresh. Every lookup follows the record's tenant, not the
|
|
20
|
+
# ambient one.
|
|
21
|
+
#
|
|
22
|
+
# @api private
|
|
23
|
+
module HasPermissionSetUsers
|
|
24
|
+
extend ActiveSupport::Concern
|
|
25
|
+
include InvalidatesInBatch
|
|
26
|
+
|
|
27
|
+
# Which Permission Sets the record's change affects, by strategy.
|
|
28
|
+
LOOKUPS = {
|
|
29
|
+
self: :self_permission_set_ids,
|
|
30
|
+
direct: :direct_permission_set_ids,
|
|
31
|
+
via_resource: :via_resource_permission_set_ids
|
|
32
|
+
}.freeze
|
|
33
|
+
|
|
34
|
+
included do
|
|
35
|
+
# a record moved to another set or resource: see #direct_permission_set_ids
|
|
36
|
+
after_save { %w[permission_set_id resource_id].each { |column| note_previous_value(column) } }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class_methods do
|
|
40
|
+
# @param strategy [:self, :direct, :via_resource]
|
|
41
|
+
def permission_set_user_lookup(strategy)
|
|
42
|
+
lookup = LOOKUPS.fetch(strategy) do
|
|
43
|
+
raise ArgumentError, "Unknown strategy: #{strategy}. Use :self, :direct, or :via_resource"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# on the class itself: these win over PermissionsRegenerator's defaults
|
|
47
|
+
define_method(:affected_permission_set_ids) { send(lookup) }
|
|
48
|
+
define_method(:users_on_permission_set) { assignments_of_affected_sets }
|
|
49
|
+
define_method(:user_ids_on_permission_set) { holder_ids_of_affected_sets }
|
|
50
|
+
private :affected_permission_set_ids, :users_on_permission_set, :user_ids_on_permission_set
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# @return [Array<PermissionForge::PermissionSetUser>]
|
|
57
|
+
def assignments_of_affected_sets
|
|
58
|
+
affected_permission_set_ids.flat_map { |set_id| permission_set_assignments(set_id).to_a }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @return [Array] the holders' user ids, without duplicates
|
|
62
|
+
def holder_ids_of_affected_sets
|
|
63
|
+
affected_permission_set_ids.flat_map { |set_id| permission_set_user_ids(set_id) }.uniq
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self_permission_set_ids
|
|
67
|
+
[id]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# A record moved to another Permission Set (or resource) changes what
|
|
71
|
+
# both grant, so the users of both are affected — every set it left
|
|
72
|
+
# since the last commit (InvalidatesInBatch#note_previous_value).
|
|
73
|
+
def direct_permission_set_ids
|
|
74
|
+
[permission_set_id, *previous_values('permission_set_id')].compact_blank.uniq.reject do |set_id|
|
|
75
|
+
shared_lookup(:inactive_permission_set, set_id) { permission_set_exists?(set_id, active: false) }
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def via_resource_permission_set_ids
|
|
80
|
+
[resource_id, *previous_values('resource_id')].compact_blank.uniq.filter_map do |parent_id|
|
|
81
|
+
set_id = shared_lookup(:permission_set_of_resource, parent_id) do
|
|
82
|
+
in_record_tenant(PermissionForge::Resource).where(id: parent_id).pick(:permission_set_id)
|
|
83
|
+
end
|
|
84
|
+
next if set_id.blank?
|
|
85
|
+
|
|
86
|
+
set_id if shared_lookup(:active_permission_set, set_id) { permission_set_exists?(set_id, active: true) }
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# The assignments of a Permission Set in this record's tenant (see
|
|
91
|
+
# InvalidatesInBatch#in_record_tenant).
|
|
92
|
+
def permission_set_assignments(set_id)
|
|
93
|
+
shared_lookup(:assignments, set_id) do
|
|
94
|
+
in_record_tenant(PermissionForge::PermissionSetUser).where(permission_set_id: set_id)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Their user ids, plucked: a set may have thousands of holders, and
|
|
99
|
+
# nothing but the id is needed to invalidate them.
|
|
100
|
+
def permission_set_user_ids(set_id)
|
|
101
|
+
shared_lookup(:assignment_user_ids, set_id) { permission_set_assignments(set_id).distinct.pluck(:user_id) }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def permission_set_exists?(set_id, active:)
|
|
105
|
+
in_record_tenant(PermissionForge::PermissionSet).where(id: set_id, is_active: active).exists?
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# The lookups depend on the record's tenant, so it is part of every key:
|
|
109
|
+
# records of two tenants in one commit do not share an answer.
|
|
110
|
+
def shared_lookup(*key, &)
|
|
111
|
+
invalidation_batch.fetch([permission_record_tenant_id, *key], &)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Concerns
|
|
5
|
+
# Conditionally includes acts_as_tenant on PermissionForge models
|
|
6
|
+
# when `config.tenant_model` is set and the acts_as_tenant gem is available.
|
|
7
|
+
#
|
|
8
|
+
# All permission data is strictly scoped to the current tenant.
|
|
9
|
+
# Cross-tenant user sharing is supported: the same user can be assigned
|
|
10
|
+
# to permission sets in multiple tenants, each producing a separate cached hash.
|
|
11
|
+
#
|
|
12
|
+
# @example Enable in initializer
|
|
13
|
+
# config.tenant_model = 'Account' # enables multi-tenancy
|
|
14
|
+
# config.tenant_column = :account_id # optional, inferred from model
|
|
15
|
+
#
|
|
16
|
+
# @api private
|
|
17
|
+
module HasTenantScoping
|
|
18
|
+
extend ActiveSupport::Concern
|
|
19
|
+
|
|
20
|
+
class_methods do
|
|
21
|
+
# Declares the tenant association and default scope on the including
|
|
22
|
+
# model, on the configured column. Each model calls it as its class
|
|
23
|
+
# body loads — after the host's config/initializers (the models are
|
|
24
|
+
# autoloaded), and after its own macros, so acts_as_tenant's callbacks
|
|
25
|
+
# keep running after the model's own; a code reload declares it again.
|
|
26
|
+
# Idempotent: acts_as_tenant adds a default_scope on every call, so
|
|
27
|
+
# applying twice (the class body plus a host, or the test suite,
|
|
28
|
+
# calling it by hand) would filter the tenant twice.
|
|
29
|
+
def apply_tenant_scoping!
|
|
30
|
+
config = PermissionForge.configuration
|
|
31
|
+
return unless config.tenant_enabled? && defined?(ActsAsTenant)
|
|
32
|
+
return if tenant_scoping_applied?
|
|
33
|
+
|
|
34
|
+
acts_as_tenant config.tenant_association_name,
|
|
35
|
+
foreign_key: config.resolved_tenant_column,
|
|
36
|
+
class_name: config.tenant_model,
|
|
37
|
+
**tenant_scoping_options
|
|
38
|
+
@pf_tenant_model = config.tenant_model.to_s
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Extra acts_as_tenant options of the including model. A permission
|
|
42
|
+
# row needs a tenant whatever the host's belongs_to_required_by_default
|
|
43
|
+
# says (the models load after the host's initializers, where that
|
|
44
|
+
# setting may be anything); a model whose rows may belong to none (an
|
|
45
|
+
# audit event of a cross-tenant task) overrides this with
|
|
46
|
+
# `{ optional: true }`.
|
|
47
|
+
def tenant_scoping_options
|
|
48
|
+
{ optional: false }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The tenant model this class was scoped on as it loaded; nil while
|
|
52
|
+
# it is not scoped (ModelBindings compares it with the configuration).
|
|
53
|
+
#
|
|
54
|
+
# @return [String, nil]
|
|
55
|
+
def tenant_scoping_model
|
|
56
|
+
@pf_tenant_model if tenant_scoping_applied?
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# acts_as_tenant defines scoped_by_tenant? on the class once applied.
|
|
60
|
+
def tenant_scoping_applied?
|
|
61
|
+
respond_to?(:scoped_by_tenant?)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The rows of one tenant whatever tenant is ambient, together with the
|
|
65
|
+
# rows that have no tenant (written before tenancy was switched on).
|
|
66
|
+
# No tenant id means every row — and so does a table that has no
|
|
67
|
+
# tenant column (an install that predates it): its rows cannot be told
|
|
68
|
+
# apart, and a lookup that raised would invalidate nobody.
|
|
69
|
+
# Built without ActsAsTenant.with_tenant, which writes the tenant it
|
|
70
|
+
# resolved back into the request-level one.
|
|
71
|
+
#
|
|
72
|
+
# @param tenant_id [Object, nil]
|
|
73
|
+
# @return [ActiveRecord::Relation]
|
|
74
|
+
def of_tenant(tenant_id)
|
|
75
|
+
config = PermissionForge.configuration
|
|
76
|
+
return all unless config.tenant_enabled? && tenant_scoping_applied?
|
|
77
|
+
|
|
78
|
+
column = config.resolved_tenant_column
|
|
79
|
+
return unscoped if tenant_id.nil? || !column_names.include?(column.to_s)
|
|
80
|
+
|
|
81
|
+
unscoped.where(column => [tenant_id, nil])
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module PermissionForge
|
|
2
|
+
# The concerns a host includes in its own classes (`PermissionedUser`, `PermissionScopedFindable`,
|
|
3
|
+
# `MaskedAttributes`, `ControllerActionGuard`, ...); the ones the gem uses on its own models are `@api private`.
|
|
4
|
+
#
|
|
5
|
+
# @api public
|
|
6
|
+
module Concerns
|
|
7
|
+
# @api private
|
|
8
|
+
module HasUuid
|
|
9
|
+
extend ActiveSupport::Concern
|
|
10
|
+
|
|
11
|
+
included do
|
|
12
|
+
before_create :generate_uuid, if: -> { respond_to?(:uuid) && uuid.blank? }
|
|
13
|
+
|
|
14
|
+
validates :uuid, uniqueness: true, allow_nil: true
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def generate_uuid
|
|
20
|
+
self.uuid = PermissionForge.configuration.uuid_generator.call
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Concerns
|
|
5
|
+
# Runs a permission record's cache invalidation after commit, through the
|
|
6
|
+
# InvalidationBatch shared by the records written in the same transaction,
|
|
7
|
+
# so what one of them invalidated is not invalidated again by the next.
|
|
8
|
+
#
|
|
9
|
+
# The including concern or model says what to invalidate by overriding
|
|
10
|
+
# `invalidate_committed_caches` (calling super) and doing the work through
|
|
11
|
+
# `invalidation_batch`:
|
|
12
|
+
#
|
|
13
|
+
# def invalidate_committed_caches
|
|
14
|
+
# super
|
|
15
|
+
# invalidation_batch.once([:my_cache, id]) { MyCache.drop(id) }
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# The rule this concern keeps: a record's after_commit reuses a batch only
|
|
19
|
+
# when the record joined it for the write being reported.
|
|
20
|
+
# - Every write that can lead to after_commit sets the reference first:
|
|
21
|
+
# save and destroy join the open batch in a before callback (it runs
|
|
22
|
+
# before the row is written, whatever other callbacks do); touch, which
|
|
23
|
+
# has no before callback, drops the reference and so invalidates on its
|
|
24
|
+
# own.
|
|
25
|
+
# - Every such write also tells the batches of this process to forget
|
|
26
|
+
# what they remember (InvalidationBatch.note_write): it may be a write
|
|
27
|
+
# made by a callback between two after_commits of one commit.
|
|
28
|
+
# - after_commit takes the reference (it is used once) and claims the
|
|
29
|
+
# batch; a refused claim, or no reference, means a batch of its own.
|
|
30
|
+
# - Outside after_commit there is no batch to reuse: calling an
|
|
31
|
+
# invalidation method by hand always invalidates.
|
|
32
|
+
#
|
|
33
|
+
# Two more things hold for every invalidation made here:
|
|
34
|
+
# - It follows the RECORD's tenant, not the ambient one. A record of
|
|
35
|
+
# tenant A saved while tenant B (or none) is ambient still finds the
|
|
36
|
+
# assignments of tenant A (`in_record_tenant`), and their users
|
|
37
|
+
# whatever default scope the host's user model has.
|
|
38
|
+
# - Nothing is raised out of after_commit: Rails would skip the commit
|
|
39
|
+
# callbacks of the remaining records. A failure is reported through
|
|
40
|
+
# Cache::Failure instead.
|
|
41
|
+
#
|
|
42
|
+
# @api private
|
|
43
|
+
module InvalidatesInBatch
|
|
44
|
+
extend ActiveSupport::Concern
|
|
45
|
+
|
|
46
|
+
included do
|
|
47
|
+
before_save :join_invalidation_batch
|
|
48
|
+
before_destroy :join_invalidation_batch
|
|
49
|
+
after_commit :invalidate_after_commit, on: %i[create update destroy]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def touch(*, **)
|
|
53
|
+
PermissionForge::InvalidationBatch.note_write
|
|
54
|
+
@pf_joined_invalidation_batch = nil
|
|
55
|
+
super
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def join_invalidation_batch
|
|
61
|
+
self.class.connection_pool.with_connection do |connection|
|
|
62
|
+
@pf_joined_invalidation_batch = PermissionForge::InvalidationBatch.join(connection)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def invalidate_after_commit
|
|
67
|
+
outer = @pf_invalidation_batch
|
|
68
|
+
joined = @pf_joined_invalidation_batch
|
|
69
|
+
@pf_joined_invalidation_batch = nil
|
|
70
|
+
@pf_invalidation_batch = joined&.claim
|
|
71
|
+
invalidate_committed_caches
|
|
72
|
+
rescue StandardError => e
|
|
73
|
+
PermissionForge::Cache::Failure.report(operation: :after_commit, error: e, record: "#{self.class.name}##{id}")
|
|
74
|
+
ensure
|
|
75
|
+
# nil, unless an invalidation wrote this very record again and its
|
|
76
|
+
# commit ran inside this one.
|
|
77
|
+
@pf_invalidation_batch = outer
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Leaves a note in the batch this record joined for the write in
|
|
81
|
+
# progress, next to the note the record keeps for itself (see
|
|
82
|
+
# InvalidationBatch#note).
|
|
83
|
+
def note_in_invalidation_batch(kind, value)
|
|
84
|
+
@pf_joined_invalidation_batch&.note(kind, value)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# A record moved by a save (another user, Permission Set or resource)
|
|
88
|
+
# changes what the previous one grants too. Call from after_save: the
|
|
89
|
+
# value `column` had before the save just made is noted when it
|
|
90
|
+
# changed — at every save, since after_commit only sees the last save
|
|
91
|
+
# of a transaction. Like InvalidatesManagedActions' note it is only ever
|
|
92
|
+
# consumed by a commit (#previous_values), never forgotten on rollback
|
|
93
|
+
# (a later save rolled back to a savepoint must not take an earlier
|
|
94
|
+
# move with it; an extra invalidation costs a rebuild), and it is also
|
|
95
|
+
# left in the batch, under the row it belongs to: of two instances of
|
|
96
|
+
# one row written in a transaction, Rails runs the commit callbacks of
|
|
97
|
+
# one.
|
|
98
|
+
def note_previous_value(column)
|
|
99
|
+
return unless has_attribute?(column) && saved_change_to_attribute?(column)
|
|
100
|
+
|
|
101
|
+
previous = attribute_before_last_save(column)
|
|
102
|
+
return if previous.nil?
|
|
103
|
+
|
|
104
|
+
((@pf_previous_values ||= {})[column] ||= []) << previous
|
|
105
|
+
note_in_invalidation_batch(:"previous_#{column}", [invalidation_row, previous])
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# The values `column` had before the saves this commit reports — this
|
|
109
|
+
# instance's, and those the batch holds for this row (another instance
|
|
110
|
+
# of it) — consumed: the next commit starts from nothing.
|
|
111
|
+
def previous_values(column)
|
|
112
|
+
own = @pf_previous_values&.delete(column) || []
|
|
113
|
+
row = invalidation_row
|
|
114
|
+
noted = invalidation_batch.noted(:"previous_#{column}").filter_map { |noted_row, value| value if noted_row == row }
|
|
115
|
+
(own + noted).uniq
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Which row a note in the batch belongs to.
|
|
119
|
+
def invalidation_row
|
|
120
|
+
[self.class.base_class.name, id]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# What to invalidate once the record's transaction has committed.
|
|
124
|
+
# Override, call super, and work through `invalidation_batch`.
|
|
125
|
+
def invalidate_committed_caches; end
|
|
126
|
+
|
|
127
|
+
# The batch of the commit being reported, inside after_commit; anywhere
|
|
128
|
+
# else a batch of its own each time, which remembers nothing.
|
|
129
|
+
def invalidation_batch
|
|
130
|
+
@pf_invalidation_batch || PermissionForge::InvalidationBatch.new
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Invalidates the cached hash of the users who hold the assignments —
|
|
134
|
+
# once per user and tenant in `batch`. By id, in slices, when nothing
|
|
135
|
+
# but the store calls of User#invalidate_permissions would happen
|
|
136
|
+
# (Cache::HolderInvalidation.by_id?); else through the public
|
|
137
|
+
# User#invalidate_permissions of each loaded user (host apps override
|
|
138
|
+
# it). A user whose invalidation raises is reported and counts as
|
|
139
|
+
# done: the others still get theirs, and the next record of the commit
|
|
140
|
+
# does not try again.
|
|
141
|
+
#
|
|
142
|
+
# @param user_ids [Array] the holders' user ids
|
|
143
|
+
def invalidate_assigned_users(user_ids, batch, tenant_id)
|
|
144
|
+
pending = user_ids.compact.uniq
|
|
145
|
+
pending.reject! { |user_id| batch.done?([:user, user_id, tenant_id]) }
|
|
146
|
+
return if pending.empty?
|
|
147
|
+
|
|
148
|
+
if PermissionForge::Cache::HolderInvalidation.by_id?(PermissionForge.configuration.user_model)
|
|
149
|
+
invalidate_user_ids(pending, batch, tenant_id)
|
|
150
|
+
else
|
|
151
|
+
assigned_users(pending).each do |user|
|
|
152
|
+
batch.once([:user, user.id, tenant_id]) { invalidate_user(user, tenant_id) }
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# What the store did not confirm was reported and owed by
|
|
158
|
+
# HolderInvalidation, and counts as done like a user whose
|
|
159
|
+
# invalidate_permissions failed.
|
|
160
|
+
def invalidate_user_ids(user_ids, batch, tenant_id)
|
|
161
|
+
begin
|
|
162
|
+
PermissionForge::Cache::HolderInvalidation.call(user_ids: user_ids, tenant_id: tenant_id)
|
|
163
|
+
rescue StandardError => e
|
|
164
|
+
PermissionForge::Cache::Failure.report(operation: :invalidate, error: e, record: "#{self.class.name}##{id}")
|
|
165
|
+
end
|
|
166
|
+
user_ids.each { |user_id| batch.done!([:user, user_id, tenant_id]) }
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def invalidate_user(user, tenant_id)
|
|
170
|
+
user.invalidate_permissions(tenant_id: tenant_id)
|
|
171
|
+
rescue StandardError => e
|
|
172
|
+
PermissionForge::Cache::Failure.report(operation: :invalidate, user_id: user.id, error: e)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Loaded past the default scopes of the host's user model: an ambient
|
|
176
|
+
# tenant, a soft-delete or a permission scope there must not hide the
|
|
177
|
+
# holder of an assignment from an invalidation.
|
|
178
|
+
def assigned_users(user_ids)
|
|
179
|
+
return [] if user_ids.empty?
|
|
180
|
+
|
|
181
|
+
model = PermissionForge.configuration.user_model
|
|
182
|
+
PermissionForge::Concerns::AutoPermissionScoping.internally_suspended { model.unscoped.where(model.primary_key => user_ids).to_a }
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# A relation over a permission model that follows this record's tenant
|
|
186
|
+
# instead of the ambient one (HasTenantScoping.of_tenant). The ambient
|
|
187
|
+
# default scope would find nothing for a record of tenant A saved under
|
|
188
|
+
# tenant B, and under no tenant it would also match another tenant's
|
|
189
|
+
# rows that merely share an id. A record without a tenant looks
|
|
190
|
+
# everywhere.
|
|
191
|
+
def in_record_tenant(model)
|
|
192
|
+
model.of_tenant(permission_record_tenant_id)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Tenant id of the changed record, so invalidation reaches the
|
|
196
|
+
# tenant-scoped cache key even when no ambient tenant is set
|
|
197
|
+
# (e.g., callbacks fired from a background job).
|
|
198
|
+
def permission_record_tenant_id
|
|
199
|
+
config = PermissionForge.configuration
|
|
200
|
+
return nil unless config.tenant_enabled?
|
|
201
|
+
|
|
202
|
+
column = config.resolved_tenant_column
|
|
203
|
+
respond_to?(column) ? public_send(column) : nil
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Concerns
|
|
5
|
+
# Drops ControllerActions::ManagedIndex when a record that defines it
|
|
6
|
+
# changes: a row that counts being created, destroyed or renamed, or a row
|
|
7
|
+
# or Permission Set being switched on or off. Flag edits (CRUD toggles,
|
|
8
|
+
# is_permitted) leave the index alone — they change who is granted, not
|
|
9
|
+
# what is managed.
|
|
10
|
+
#
|
|
11
|
+
# The change is noted at each save and consumed at commit. after_commit on
|
|
12
|
+
# its own sees only the LAST save of a transaction: a row created (or
|
|
13
|
+
# switched on) and then saved again would look unchanged there, and the
|
|
14
|
+
# guard would keep treating its action as unmanaged.
|
|
15
|
+
#
|
|
16
|
+
# A note is only ever consumed by a commit, never forgotten: a later save
|
|
17
|
+
# rolled back to a savepoint must not take the note of an earlier save
|
|
18
|
+
# with it. After a rollback the note stays, and the record's next commit
|
|
19
|
+
# drops the index once more than needed — which costs one rebuild, where a
|
|
20
|
+
# missed drop lets requests through for as long as the index lives.
|
|
21
|
+
#
|
|
22
|
+
# The note is also left in the transaction's InvalidationBatch: of two
|
|
23
|
+
# instances of one row written in a transaction, Rails runs the commit
|
|
24
|
+
# callbacks of one, which may not be the one that changed what is managed.
|
|
25
|
+
#
|
|
26
|
+
# The index is dropped after commit through InvalidatesInBatch: once per
|
|
27
|
+
# transaction (and tenant), however many of its records changed what is
|
|
28
|
+
# managed.
|
|
29
|
+
#
|
|
30
|
+
# @api private
|
|
31
|
+
module InvalidatesManagedActions
|
|
32
|
+
extend ActiveSupport::Concern
|
|
33
|
+
include InvalidatesInBatch
|
|
34
|
+
|
|
35
|
+
NOTE = :managed_actions
|
|
36
|
+
|
|
37
|
+
included do
|
|
38
|
+
after_save :note_managed_actions_change
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def note_managed_actions_change
|
|
44
|
+
return unless managed_actions_changed?
|
|
45
|
+
|
|
46
|
+
@pf_managed_actions_changed = true
|
|
47
|
+
note_in_invalidation_batch(NOTE, permission_record_tenant_id)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The index first, and each of the two even when the other raises.
|
|
51
|
+
def invalidate_committed_caches
|
|
52
|
+
invalidate_managed_actions
|
|
53
|
+
ensure
|
|
54
|
+
super
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def invalidate_managed_actions
|
|
58
|
+
batch = invalidation_batch
|
|
59
|
+
tenant_ids = batch.noted(NOTE)
|
|
60
|
+
tenant_ids |= [permission_record_tenant_id] if destroyed? ? is_active : @pf_managed_actions_changed
|
|
61
|
+
|
|
62
|
+
tenant_ids.each do |tenant_id|
|
|
63
|
+
batch.once([NOTE, tenant_id]) do
|
|
64
|
+
PermissionForge::ControllerActions::ManagedIndex.invalidate!(tenant_id: tenant_id)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
@pf_managed_actions_changed = false
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Whether the save just made changed what is managed. For a Resource or
|
|
71
|
+
# ResourceAction: a new row that counts, a row switched on or off, or a
|
|
72
|
+
# rename of a row that counts. PermissionSet overrides this.
|
|
73
|
+
def managed_actions_changed?
|
|
74
|
+
return is_active if previously_new_record?
|
|
75
|
+
|
|
76
|
+
saved_change_to_is_active? || (is_active && saved_change_to_name?)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|