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,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Cache
|
|
5
|
+
# What is invalidated once more when a transaction has committed.
|
|
6
|
+
#
|
|
7
|
+
# An invalidation made inside an open transaction comes too early for
|
|
8
|
+
# everybody else: the rows it is about are not visible to them yet, so a
|
|
9
|
+
# request that rebuilds right after it caches the rows as they were
|
|
10
|
+
# before the commit — and keeps them. (regenerate_permissions inside a
|
|
11
|
+
# transaction that reads from a snapshot leaves nothing in the cache at
|
|
12
|
+
# all, which tells nobody that something changed.) So the invalidation
|
|
13
|
+
# is made again when the outermost transaction has committed: once per
|
|
14
|
+
# key for each transaction that asked. A rollback drops it, because
|
|
15
|
+
# nothing changed.
|
|
16
|
+
#
|
|
17
|
+
# Used for a user's permission hash (PermissionCacheAccessor, inside a
|
|
18
|
+
# transaction that reads from a snapshot) and for the managed-actions
|
|
19
|
+
# index (ControllerActions::ManagedIndex, inside any open transaction).
|
|
20
|
+
#
|
|
21
|
+
# @api private
|
|
22
|
+
class Settlement
|
|
23
|
+
# The settlement of each open transaction, by the adapter's transaction
|
|
24
|
+
# object. Keys and values are held weakly: a settlement lives as long
|
|
25
|
+
# as the commit hook it is registered as.
|
|
26
|
+
PENDING = ObjectSpace::WeakMap.new
|
|
27
|
+
|
|
28
|
+
# @param transaction [PermissionForge::OpenTransaction]
|
|
29
|
+
# @return [Settlement] the one of this transaction
|
|
30
|
+
def self.for(transaction)
|
|
31
|
+
subject = transaction.subject
|
|
32
|
+
PENDING[subject] ||= new.tap { |settlement| transaction.after_commit { settlement.call } }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def initialize
|
|
36
|
+
@work = {}
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @param key [Object] what is invalidated (e.g. [:user, id, tenant_id]):
|
|
40
|
+
# the first block given for a key is the one that runs
|
|
41
|
+
# @return [void]
|
|
42
|
+
def add(key, &block)
|
|
43
|
+
@work[key] ||= block
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Runs among the commit callbacks of the transaction, so nothing is
|
|
47
|
+
# raised: Rails would skip the callbacks that come after it.
|
|
48
|
+
#
|
|
49
|
+
# @return [void]
|
|
50
|
+
def call
|
|
51
|
+
@work.each_value { |block| run(block) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def run(block)
|
|
57
|
+
block.call
|
|
58
|
+
rescue StandardError => e
|
|
59
|
+
Failure.report(operation: :after_commit, error: e)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Cache
|
|
5
|
+
# Where a user's permission hash and invalidation stamp live, and what an
|
|
6
|
+
# invalidation of the user removes — one definition for
|
|
7
|
+
# User#invalidate_permissions (PermissionCacheAccessor) and for the
|
|
8
|
+
# invalidation of many holders by id (HolderInvalidation). A key added to
|
|
9
|
+
# one is added to both.
|
|
10
|
+
#
|
|
11
|
+
# The entries live under ENTRY_NAMESPACE ("v2:user_<id>",
|
|
12
|
+
# "v2:tenant_<tenant id>_user_<id>"), bumped when their format changes
|
|
13
|
+
# (0.14: the base entry of a user with conditional sets carries them under
|
|
14
|
+
# :conditional_sets), so that during a rolling deploy a process running
|
|
15
|
+
# another version never reads an entry it would misread. An invalidation
|
|
16
|
+
# removes the keys of the previous namespaces as well
|
|
17
|
+
# (PREVIOUS_ENTRY_NAMESPACES), so a change made through either version
|
|
18
|
+
# reaches both. The stamp is not namespaced: every version must see it.
|
|
19
|
+
#
|
|
20
|
+
# An invalidation removes the base entries, and reads nothing: the
|
|
21
|
+
# per-context variants of a user with conditional sets cannot be reached
|
|
22
|
+
# once the base entry that carries their token is gone, and are left to
|
|
23
|
+
# their TTL (ConditionalSets). Where they must go now — erasure,
|
|
24
|
+
# cache:clear — .variant_removals names them: the index entry of each
|
|
25
|
+
# base entry of this namespace is read (a few bytes, written only for a
|
|
26
|
+
# user who had a variant stored; one call for many users where the store
|
|
27
|
+
# can, Base#read_many), and every slot of a base entry that has one
|
|
28
|
+
# (ConditionalSets.variant_keys) goes with it.
|
|
29
|
+
#
|
|
30
|
+
# @api private
|
|
31
|
+
module UserKeys
|
|
32
|
+
ENTRY_NAMESPACE = 'v2'
|
|
33
|
+
# '' is the bare key of 0.13. Drop in 0.15.
|
|
34
|
+
PREVIOUS_ENTRY_NAMESPACES = [''].freeze
|
|
35
|
+
|
|
36
|
+
module_function
|
|
37
|
+
|
|
38
|
+
# Every namespace an entry may be under: this version's and the previous ones.
|
|
39
|
+
#
|
|
40
|
+
# @return [Array<String>]
|
|
41
|
+
def namespaces
|
|
42
|
+
[ENTRY_NAMESPACE, *PREVIOUS_ENTRY_NAMESPACES]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# What the permission entries of a namespace look like, for a store that
|
|
46
|
+
# removes keys by pattern (Base#clear_entries): the keys of every user
|
|
47
|
+
# — any id, a prefixed string one included — tenant-agnostic and per
|
|
48
|
+
# tenant, and their variants. Nothing else of the gem under the bare
|
|
49
|
+
# namespace matches the Regexp: not the stamps (`_invalidation`), not
|
|
50
|
+
# the managed-actions index. The globs, which cannot say "not followed
|
|
51
|
+
# by", take the stamps along with the bare keys. The Regexp starts with
|
|
52
|
+
# `^`, the one anchor ActiveSupport's key_matcher keeps when it puts the
|
|
53
|
+
# store's own namespace in front (a `\A` would be prefixed with `.*`
|
|
54
|
+
# and never match).
|
|
55
|
+
#
|
|
56
|
+
# @param namespace [String]
|
|
57
|
+
# @param prefix [String] the store's key prefix (Base#prefix)
|
|
58
|
+
# @return [Array(Regexp, Array<String>)] the Regexp, and the globs
|
|
59
|
+
def entry_patterns(namespace, prefix:)
|
|
60
|
+
lead = Regexp.escape(entry_lead(namespace, prefix))
|
|
61
|
+
regexp = /^#{lead}(tenant_.+_)?user_(?!.*_invalidation\z).+\z/
|
|
62
|
+
globs = ["#{entry_lead(namespace, prefix)}user_*", "#{entry_lead(namespace, prefix)}tenant_*_user_*"]
|
|
63
|
+
[regexp, globs]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Two entries no user can have, one of each key shape, under
|
|
67
|
+
# `namespace`: written before a removal by pattern and read after it,
|
|
68
|
+
# they say whether the pattern reached the store's keys at all
|
|
69
|
+
# (Base#clear_entries).
|
|
70
|
+
#
|
|
71
|
+
# @return [Array<String>] the tenant-agnostic and the tenant key
|
|
72
|
+
def canary_keys(namespace)
|
|
73
|
+
[hash_key(CANARY, nil, namespace: namespace), hash_key(CANARY, CANARY, namespace: namespace)]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# An id no host key has (not an integer, not a uuid).
|
|
77
|
+
CANARY = 'pf-canary'
|
|
78
|
+
|
|
79
|
+
def entry_lead(namespace, prefix)
|
|
80
|
+
namespace.empty? ? "#{prefix}:" : "#{prefix}:#{namespace}:"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# The key of the user's base entry in a tenant — the ambient one unless
|
|
84
|
+
# named; nil: the tenant-agnostic key — in this namespace
|
|
85
|
+
# (PermissionCacheAccessor#permission_cache_key).
|
|
86
|
+
#
|
|
87
|
+
# @param user_id [Object]
|
|
88
|
+
# @param tenant_id [Object, nil]
|
|
89
|
+
# @param namespace [String] ENTRY_NAMESPACE, or a previous one
|
|
90
|
+
# @return [String]
|
|
91
|
+
def hash_key(user_id, tenant_id = PermissionForge.ambient_tenant_id, namespace: ENTRY_NAMESPACE)
|
|
92
|
+
key = tenant_id ? "tenant_#{tenant_id}_user_#{user_id}" : "user_#{user_id}"
|
|
93
|
+
namespace.empty? ? key : "#{namespace}:#{key}"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# One per user, whatever the tenant and the version (see Cache::Invalidation).
|
|
97
|
+
#
|
|
98
|
+
# @param user_id [Object]
|
|
99
|
+
# @return [String]
|
|
100
|
+
def stamp_key(user_id)
|
|
101
|
+
"user_#{user_id}_invalidation"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# The keys a base entry of the user may be under — in the ambient
|
|
105
|
+
# tenant, in no tenant, and in the tenant the caller named (a callback
|
|
106
|
+
# on a tenant-scoped record) — in this namespace, and in the previous
|
|
107
|
+
# ones.
|
|
108
|
+
#
|
|
109
|
+
# @param user_id [Object]
|
|
110
|
+
# @param tenant_id [Object, nil]
|
|
111
|
+
# @param ambient_key [String] the key under the ambient tenant, when
|
|
112
|
+
# the user instance names its own (#permission_cache_key)
|
|
113
|
+
# @return [Array(Array<String>, Array<String>)] this namespace's keys,
|
|
114
|
+
# the previous namespaces' keys
|
|
115
|
+
def base_keys(user_id, tenant_id: nil, ambient_key: hash_key(user_id))
|
|
116
|
+
current = ([ambient_key] + [nil, tenant_id].uniq.map { |tenant| hash_key(user_id, tenant) }).uniq
|
|
117
|
+
previous = PREVIOUS_ENTRY_NAMESPACES.flat_map { |namespace| current.map { |key| in_namespace(key, namespace) } }
|
|
118
|
+
[current, previous]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# `key`, of this namespace, in another one ('' : the bare key of 0.13).
|
|
122
|
+
def in_namespace(key, namespace)
|
|
123
|
+
bare = key.delete_prefix("#{ENTRY_NAMESPACE}:")
|
|
124
|
+
namespace.empty? ? bare : "#{namespace}:#{bare}"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# The arguments of the user's Invalidation: the stamp, and every key —
|
|
128
|
+
# this namespace's, then the previous namespaces'.
|
|
129
|
+
#
|
|
130
|
+
# @param user_id [Object]
|
|
131
|
+
# @param tenant_id [Object, nil]
|
|
132
|
+
# @param named [Hash] the keys a user instance names itself, when it
|
|
133
|
+
# does: ambient_key: (#permission_cache_key), stamp_key:
|
|
134
|
+
# (#pf_invalidation_stamp_key)
|
|
135
|
+
# @return [Hash] user_id:, stamp_key:, keys:
|
|
136
|
+
def invalidation(user_id, tenant_id: nil, **named)
|
|
137
|
+
invalidations([user_id], tenant_id: tenant_id, **named).first
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# The invalidations of many users.
|
|
141
|
+
#
|
|
142
|
+
# @param user_ids [Array]
|
|
143
|
+
# @param tenant_id [Object, nil]
|
|
144
|
+
# @return [Array<Hash>] one per user (see .invalidation)
|
|
145
|
+
def invalidations(user_ids, tenant_id: nil, **named)
|
|
146
|
+
user_ids.map do |user_id|
|
|
147
|
+
current, previous = base_keys(user_id, tenant_id: tenant_id, **named.slice(:ambient_key))
|
|
148
|
+
target(user_id, current + previous, named)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# The removals of the per-context variants of many users
|
|
153
|
+
# (Cache::VariantRemoval): for each base entry of this namespace, its
|
|
154
|
+
# index entry and every slot (ConditionalSets.variant_keys). With
|
|
155
|
+
# `indexed_only` (cache:clear, many users) only the base entries whose
|
|
156
|
+
# index entry is found — read in one call — and a user none of whose
|
|
157
|
+
# base entries has one is left out. Without it (erasure, one person)
|
|
158
|
+
# nothing is read: a store that fails a read answers "nothing", and the
|
|
159
|
+
# store may have evicted the index entry.
|
|
160
|
+
#
|
|
161
|
+
# @param user_ids [Array]
|
|
162
|
+
# @param tenant_id [Object, nil]
|
|
163
|
+
# @param named [Hash] ambient_key:, stamp_key:, as for .invalidation;
|
|
164
|
+
# indexed_only: (default true)
|
|
165
|
+
# @return [Array<Hash>] user_id:, stamp_key:, keys: — the arguments of
|
|
166
|
+
# an Invalidation
|
|
167
|
+
def variant_removals(user_ids, tenant_id: nil, **named)
|
|
168
|
+
keyed = user_ids.map { |user_id| [user_id, base_keys(user_id, tenant_id: tenant_id, **named.slice(:ambient_key)).first] }
|
|
169
|
+
indexes = keyed.flat_map { |_user_id, current| current.map { |key| index_key(key) } }
|
|
170
|
+
found = named.fetch(:indexed_only, true) ? read_indexes(indexes) : EVERY_KEY
|
|
171
|
+
keyed.filter_map do |user_id, current|
|
|
172
|
+
keys = slot_keys(current, found)
|
|
173
|
+
target(user_id, keys, named) if keys.any?
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Answers every index entry as found.
|
|
178
|
+
EVERY_KEY = Hash.new(true).freeze
|
|
179
|
+
|
|
180
|
+
# The arguments of an Invalidation of the user (stamp_key: as named, else the user's own).
|
|
181
|
+
def target(user_id, keys, named)
|
|
182
|
+
{ user_id: user_id, stamp_key: named.fetch(:stamp_key) { stamp_key(user_id) }, keys: keys }
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# For each base key whose index entry was found: the index entry and every slot.
|
|
186
|
+
def slot_keys(base_keys, found)
|
|
187
|
+
conditional = PermissionForge::ConditionalSets
|
|
188
|
+
base_keys.select { |key| found[index_key(key)] }.flat_map { |key| [index_key(key), *conditional.variant_keys(key)] }
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def index_key(key)
|
|
192
|
+
PermissionForge::ConditionalSets.index_key(key)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# A read that raises tells nothing about the index entries: each counts
|
|
196
|
+
# as found, so every slot is removed rather than none.
|
|
197
|
+
#
|
|
198
|
+
# @return [Hash{String => Object}]
|
|
199
|
+
def read_indexes(keys)
|
|
200
|
+
PermissionForge.cache.read_many(keys)
|
|
201
|
+
rescue StandardError
|
|
202
|
+
keys.to_h { |key| [key, true] }
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Cache
|
|
5
|
+
# Removes the per-context variants of users with conditional permission
|
|
6
|
+
# sets, where the personal data they hold (the profile) must go now:
|
|
7
|
+
# Services::DataSubjectErasure and the enumerated
|
|
8
|
+
# Services::PermissionCacheClearer. An invalidation leaves them to their
|
|
9
|
+
# TTL: no read can reach them once the base entry that carries their
|
|
10
|
+
# token is gone (ConditionalSets).
|
|
11
|
+
#
|
|
12
|
+
# For the clear (indexed_only, many users) the users' index entries are
|
|
13
|
+
# read (UserKeys.variant_removals, one call per SLICE users), and for each
|
|
14
|
+
# one found the index entry and every slot of its base entry go. For an
|
|
15
|
+
# erasure (one person) nothing is read: the index entry and every slot of
|
|
16
|
+
# each of the person's base entries go — a read that fails silently, or
|
|
17
|
+
# an index entry the store evicted, cannot leave a variant behind. Either
|
|
18
|
+
# way the keys are removed the way an invalidation removes them
|
|
19
|
+
# (BulkInvalidation: a fresh stamp first, every answer checked, retries,
|
|
20
|
+
# what fails reported through Cache::Failure and owed).
|
|
21
|
+
#
|
|
22
|
+
# Callers run it after the users' invalidation. A variant whose rebuild
|
|
23
|
+
# read the stamp before that invalidation was then either verified
|
|
24
|
+
# before it — its index entry, written first, is found here — or is
|
|
25
|
+
# discarded by its verification. A rebuild still in flight that reads the
|
|
26
|
+
# stamp after the removal's and writes after it leaves a variant built
|
|
27
|
+
# from the rows as they are then (its profile from that request's user
|
|
28
|
+
# instance, which may predate the change): unreachable, gone with its TTL.
|
|
29
|
+
#
|
|
30
|
+
# @api private
|
|
31
|
+
class VariantRemoval
|
|
32
|
+
def self.call(...) = new(...).call
|
|
33
|
+
|
|
34
|
+
# @param user_ids [Array]
|
|
35
|
+
# @param tenant_id [Object, nil] the tenant whose base keys to include
|
|
36
|
+
# besides the ambient and the tenant-agnostic ones (UserKeys.base_keys)
|
|
37
|
+
# @param indexed_only [Boolean] only the base entries whose index entry
|
|
38
|
+
# is found (false: every base entry, nothing read)
|
|
39
|
+
# @param named [Hash] ambient_key: — the key a single user instance
|
|
40
|
+
# names (#permission_cache_key)
|
|
41
|
+
def initialize(user_ids:, tenant_id: nil, indexed_only: true, **named)
|
|
42
|
+
@user_ids = user_ids
|
|
43
|
+
@tenant_id = tenant_id
|
|
44
|
+
@named = named.merge(indexed_only: indexed_only)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @return [Array] the ids whose removal the store did not confirm
|
|
48
|
+
def call
|
|
49
|
+
@user_ids.each_slice(HolderInvalidation::SLICE).flat_map do |slice|
|
|
50
|
+
BulkInvalidation.unconfirmed(UserKeys.variant_removals(slice, tenant_id: @tenant_id, **@named))
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Cache
|
|
5
|
+
# Says once per process that permission hashes keep being built inside a
|
|
6
|
+
# database transaction that reads from a snapshot, where they are not
|
|
7
|
+
# cached (PermissionCacheAccessor#pf_rebuild_permissions, config.snapshot_transactions).
|
|
8
|
+
#
|
|
9
|
+
# One such rebuild is nothing to mention. A job that only reads
|
|
10
|
+
# permissions inside the transaction of each save, or an application that
|
|
11
|
+
# wraps every request in a transaction, never caches a hash, though, and
|
|
12
|
+
# rebuilds the permissions every time — and nothing else would tell it
|
|
13
|
+
# why. So the rebuilds that were withheld are counted until one runs
|
|
14
|
+
# outside such a transaction again, and the count reaching AFTER is
|
|
15
|
+
# logged, once.
|
|
16
|
+
#
|
|
17
|
+
# @api private
|
|
18
|
+
module WithheldNotice
|
|
19
|
+
AFTER = 20
|
|
20
|
+
MUTEX = Mutex.new
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
# @return [void]
|
|
24
|
+
def withheld
|
|
25
|
+
say = MUTEX.synchronize do
|
|
26
|
+
@in_a_row = (@in_a_row || 0) + 1
|
|
27
|
+
@in_a_row == AFTER && !@said && (@said = true)
|
|
28
|
+
end
|
|
29
|
+
log if say
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# A rebuild ran outside a transaction: the count starts again.
|
|
33
|
+
#
|
|
34
|
+
# @return [void]
|
|
35
|
+
def not_withheld
|
|
36
|
+
MUTEX.synchronize { @in_a_row = 0 }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# For tests: the notice can be given again.
|
|
40
|
+
def reset!
|
|
41
|
+
MUTEX.synchronize { @in_a_row = @said = nil }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def log
|
|
47
|
+
return unless defined?(Rails) && Rails.logger
|
|
48
|
+
|
|
49
|
+
Rails.logger.warn(
|
|
50
|
+
"[PermissionForge] The last #{AFTER} permission rebuilds ran inside a database transaction that reads from " \
|
|
51
|
+
'a snapshot, so none of them was cached: such a hash may be older than a change that was already ' \
|
|
52
|
+
'invalidated. Read the permissions once (current_user.permissions_hash) before the loop of saves or the ' \
|
|
53
|
+
'transaction — or, if this database runs at READ COMMITTED, set config.snapshot_transactions = false. ' \
|
|
54
|
+
'This is logged once per process.'
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Concerns
|
|
5
|
+
# Association extension (`has_many ..., extend: AuditedCollection`) for
|
|
6
|
+
# the collections the gem declares on its models and on the host's User.
|
|
7
|
+
#
|
|
8
|
+
# Rails' `collection.delete_all` and `collection.clear` remove rows with
|
|
9
|
+
# one SQL statement and run no callbacks — even under
|
|
10
|
+
# `dependent: :destroy` — so an assignment or membership removed that way
|
|
11
|
+
# would leave no audit event and keep the person's cached permissions.
|
|
12
|
+
# Here they remove each record the way `collection.delete(record)` does
|
|
13
|
+
# (destroy under `dependent: :destroy`, the `before_remove` callbacks
|
|
14
|
+
# otherwise), inside one transaction.
|
|
15
|
+
#
|
|
16
|
+
# An explicit strategy (`delete_all(:delete_all)`, `delete_all(:nullify)`)
|
|
17
|
+
# and a narrowed relation (`collection.where(...).delete_all`, which
|
|
18
|
+
# Rails extends with this module too) are the caller asking for SQL and
|
|
19
|
+
# are left to Rails, like `Model.where(...).delete_all`.
|
|
20
|
+
#
|
|
21
|
+
# @api public
|
|
22
|
+
module AuditedCollection
|
|
23
|
+
# @return [Integer] the number of records removed
|
|
24
|
+
def delete_all(dependent = nil)
|
|
25
|
+
return super if dependent
|
|
26
|
+
return super() unless is_a?(ActiveRecord::Associations::CollectionProxy)
|
|
27
|
+
|
|
28
|
+
records = load_target.dup
|
|
29
|
+
delete(*records) if records.any?
|
|
30
|
+
records.size
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Concerns
|
|
5
|
+
# Level 1 — ENFORCE (config.auto_query_scoping = :filter_only).
|
|
6
|
+
#
|
|
7
|
+
# Class methods backing the default_scope installed by
|
|
8
|
+
# {PermissionScopedFindable}: every relation built on a protected model
|
|
9
|
+
# applies the current user's FILTER-type rules automatically. Extracted
|
|
10
|
+
# from PermissionScopedFindable for SRP — this module owns the auto-scope
|
|
11
|
+
# decision tree, the host concern owns find_by/permission_scoped.
|
|
12
|
+
#
|
|
13
|
+
# Every path that lets a query past the rules while Level 1 is on is
|
|
14
|
+
# published as rule.bypassed (scope_suspended, auto_scope_opt_out,
|
|
15
|
+
# nil_user, view_all, modify_all, no_rules) and a nil user under :deny /
|
|
16
|
+
# :raise as access.denied — at most once per request per model, reason
|
|
17
|
+
# and user (Audit::Runtime.once?): this runs on every relation built.
|
|
18
|
+
#
|
|
19
|
+
# @api private
|
|
20
|
+
module AutoPermissionScoping
|
|
21
|
+
extend ActiveSupport::Concern
|
|
22
|
+
|
|
23
|
+
# Set while the gem suspends Level 1 for its own queries
|
|
24
|
+
# (internally_suspended): that suspension is not the host's, is not
|
|
25
|
+
# reported, and gives nobody signed in no bypass.
|
|
26
|
+
INTERNAL_SUSPENSION_KEY = :pf_internal_scope_suspension
|
|
27
|
+
|
|
28
|
+
# Set while the default scope builds the rules (re-entrancy guard).
|
|
29
|
+
IN_HOOK_KEY = :pf_in_auto_scope_hook
|
|
30
|
+
|
|
31
|
+
# The gem's own suspension of the scoping, for the queries it makes for
|
|
32
|
+
# itself and the host code they run (callbacks, default scopes): Level 1
|
|
33
|
+
# and detection are off, as in the host's block, but it is not the
|
|
34
|
+
# host's opt-out — not reported as scope_suspended, and nobody signed in
|
|
35
|
+
# gets no bypass of nil_user_strategy. Both flags are :fiber slots of
|
|
36
|
+
# ExecutionState, so they move together.
|
|
37
|
+
def self.internally_suspended(&)
|
|
38
|
+
PermissionForge::ExecutionState.with(INTERNAL_SUSPENSION_KEY, true) do
|
|
39
|
+
PermissionForge.without_permission_scope(&)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @return [Boolean] whether the gem suspended the scoping for itself
|
|
44
|
+
def self.internally_suspended?
|
|
45
|
+
PermissionForge::ExecutionState[INTERNAL_SUSPENSION_KEY] == true
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @return [Boolean] whether the host suspended the scoping
|
|
49
|
+
# (PermissionForge.without_permission_scope), as opposed to the gem
|
|
50
|
+
# for its own queries
|
|
51
|
+
def self.host_suspended?
|
|
52
|
+
PermissionForge.scope_suspended? && !internally_suspended?
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
class_methods do
|
|
56
|
+
# Level 1 hook body. Public because the default_scope block delegates
|
|
57
|
+
# to the model class through Relation#method_missing (public dispatch).
|
|
58
|
+
#
|
|
59
|
+
# Every path taken while Level 1 is enabled annotates the relation as
|
|
60
|
+
# scoped — including bypasses (view_all, no rules, nil-user :bypass):
|
|
61
|
+
# the query went through the permission check, so Level 2 detection
|
|
62
|
+
# must treat it as compliant and flag only true escapes
|
|
63
|
+
# (unscoped, find_by_sql, raw SQL).
|
|
64
|
+
#
|
|
65
|
+
# @return [ActiveRecord::Relation]
|
|
66
|
+
def pf_auto_permission_scope
|
|
67
|
+
return all unless pf_auto_scope_applicable?
|
|
68
|
+
# Re-entrancy guard: current_user_resolver or a permissions rebuild
|
|
69
|
+
# may itself query a protected model — that inner query runs unscoped
|
|
70
|
+
# (identity resolution precedes authorization) instead of recursing.
|
|
71
|
+
return all.annotate(PermissionScopedFindable::SCOPED_ANNOTATION) if PermissionForge::ExecutionState[IN_HOOK_KEY]
|
|
72
|
+
|
|
73
|
+
PermissionForge::ExecutionState.with(IN_HOOK_KEY, true) do
|
|
74
|
+
pf_build_auto_scope.annotate(PermissionScopedFindable::SCOPED_ANNOTATION)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
# The same three conditions as ever; the order only decides which
|
|
81
|
+
# bypass is reported (none while Level 1 is off).
|
|
82
|
+
def pf_auto_scope_applicable?
|
|
83
|
+
return false unless PermissionForge.configuration.auto_query_scoping_enabled?
|
|
84
|
+
|
|
85
|
+
if PermissionForge.scope_suspended?
|
|
86
|
+
pf_auto_scope_bypassed(:scope_suspended) if AutoPermissionScoping.host_suspended?
|
|
87
|
+
return false
|
|
88
|
+
end
|
|
89
|
+
return true if permission_forge_auto_scope
|
|
90
|
+
|
|
91
|
+
pf_auto_scope_bypassed(:auto_scope_opt_out)
|
|
92
|
+
false
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def pf_auto_scope_bypassed(reason, user = nil)
|
|
96
|
+
return unless PermissionForge::Audit::Runtime::BYPASS_GATE.open?
|
|
97
|
+
|
|
98
|
+
PermissionForge::Audit::Runtime.bypassed(reason, model: self, via: 'auto_scope', user: user, once: true)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# The rules do not apply: reported, and the relation as it is.
|
|
102
|
+
def pf_unscoped_by(reason, user)
|
|
103
|
+
pf_auto_scope_bypassed(reason, user)
|
|
104
|
+
all
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def pf_build_auto_scope
|
|
108
|
+
current_user = PermissionForge.current_user
|
|
109
|
+
return pf_nil_user_auto_scope unless current_user
|
|
110
|
+
|
|
111
|
+
# Resolve the rules once — permissions_hash is a cache read per
|
|
112
|
+
# call, and this hook runs on every relation build: those of this
|
|
113
|
+
# class's Resource entry and of its STI ancestors' (ResourceChain).
|
|
114
|
+
# view_all/modify_all bypasses rules (parity with find_by). The
|
|
115
|
+
# on_rule_bypass hook is deliberately NOT fired here — it would fire
|
|
116
|
+
# on every query the user runs; rule.bypassed is, once per request.
|
|
117
|
+
rules, bypass = PermissionForge::ResourceChain.rules(current_user, PermissionForge::ResourceChain.names(self))
|
|
118
|
+
return pf_unscoped_by(bypass, current_user) if bypass
|
|
119
|
+
|
|
120
|
+
rules = rules.select { |rule| pf_filter_rule?(rule) }
|
|
121
|
+
return pf_unscoped_by(:no_rules, current_user) if rules.empty?
|
|
122
|
+
|
|
123
|
+
# INVARIANT: PermissionRuleApplier builds from @model_class.where(nil),
|
|
124
|
+
# which inside this default_scope block resolves against the in-flight
|
|
125
|
+
# current_scope — preserving acts_as_tenant and any host default_scope
|
|
126
|
+
# (soft-delete) in either declaration order. Never change the applier
|
|
127
|
+
# to unscoped/fresh relations.
|
|
128
|
+
PermissionForge::Services::PermissionRuleApplier.scope(
|
|
129
|
+
model_class: self, rules: rules, current_user: current_user
|
|
130
|
+
)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Fail closed for :deny AND :raise — raising from inside a default_scope
|
|
134
|
+
# would detonate in consoles, rake tasks, and view-layer counts, so the
|
|
135
|
+
# strictest safe behavior is an empty relation (documented deviation).
|
|
136
|
+
def pf_nil_user_auto_scope
|
|
137
|
+
case PermissionForge.configuration.nil_user_strategy
|
|
138
|
+
when :deny, :raise
|
|
139
|
+
PermissionForge::Audit::Runtime.denied(PermissionForge::Audit::Runtime::DENIED, reason: :nil_user, model: self,
|
|
140
|
+
via: 'auto_scope', once: true)
|
|
141
|
+
none
|
|
142
|
+
else # :bypass
|
|
143
|
+
pf_auto_scope_bypassed(:nil_user)
|
|
144
|
+
all
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Filter-type rules only: join-type rules inside a default_scope would
|
|
149
|
+
# break includes/count/pluck and duplicate rows, so they remain the job
|
|
150
|
+
# of permission_scoped (a boot warning fires when join rules exist while
|
|
151
|
+
# Level 1 is enabled).
|
|
152
|
+
def pf_filter_rule?(rule)
|
|
153
|
+
PermissionForge::PermissionSetRule.filter_join_type?(rule[:join_type])
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Concerns
|
|
5
|
+
# Runtime guard for host controllers: checks every action against the
|
|
6
|
+
# current user's Permission Sets before it runs.
|
|
7
|
+
#
|
|
8
|
+
# class CustomersController < ApplicationController
|
|
9
|
+
# include PermissionForge::Concerns::ControllerActionGuard
|
|
10
|
+
# end
|
|
11
|
+
#
|
|
12
|
+
# class Customers::MergesController < ApplicationController
|
|
13
|
+
# include PermissionForge::Concerns::ControllerActionGuard
|
|
14
|
+
# permission_forge_actions resource: 'Customer', actions: { create: :merge }
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# The guard never renders or redirects. A denied action raises
|
|
18
|
+
# PermissionForge::ActionNotAuthorizedError — rescue it in the host app:
|
|
19
|
+
#
|
|
20
|
+
# rescue_from PermissionForge::ActionNotAuthorizedError do
|
|
21
|
+
# head :forbidden
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# The decision itself lives in {PermissionForge::ControllerActions::Authorizer}.
|
|
25
|
+
#
|
|
26
|
+
# @api public
|
|
27
|
+
module ControllerActionGuard
|
|
28
|
+
extend ActiveSupport::Concern
|
|
29
|
+
|
|
30
|
+
included do
|
|
31
|
+
# The controller's declaration, in the shape ControllerActions::Mapping
|
|
32
|
+
# reads. Class-level only: a public instance reader would be routable
|
|
33
|
+
# as a controller action.
|
|
34
|
+
class_attribute :pf_action_guard_options, instance_accessor: false, instance_predicate: false,
|
|
35
|
+
default: PermissionForge::ControllerActions::EMPTY_OPTIONS
|
|
36
|
+
|
|
37
|
+
before_action :pf_authorize_action!
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
class_methods do
|
|
41
|
+
# Declares which Resource and action keys this controller's actions
|
|
42
|
+
# mean. Subclasses inherit the declaration. Declaring again — in the
|
|
43
|
+
# same class or a subclass — adds to it: a given resource or
|
|
44
|
+
# rest_actions replaces, actions are merged.
|
|
45
|
+
#
|
|
46
|
+
# @param resource [String, Symbol, Class, nil] Resource name; nil keeps
|
|
47
|
+
# the convention (controller name → model)
|
|
48
|
+
# @param actions [Hash] controller action name => action key
|
|
49
|
+
# (`{ create: :merge }`); a CRUD word (`:edit`) checks that CRUD flag
|
|
50
|
+
# @param rest_actions [Boolean, nil] false leaves index/show/new/create/
|
|
51
|
+
# edit/update/destroy unchecked unless listed in `actions`; nil keeps
|
|
52
|
+
# the current setting (checked, unless an earlier declaration said false)
|
|
53
|
+
# @return [Hash] the normalized, frozen declaration
|
|
54
|
+
# @raise [ArgumentError] when the resource or a key is not a valid name
|
|
55
|
+
def permission_forge_actions(resource: nil, actions: {}, rest_actions: nil)
|
|
56
|
+
self.pf_action_guard_options = PermissionForge::ControllerActions.merge_options(
|
|
57
|
+
pf_action_guard_options, resource: resource, actions: actions, rest_actions: rest_actions
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
# A denial is published as action.denied, an allowed request that
|
|
65
|
+
# skipped the Permission Sets (no user under :bypass, the bypass lambda,
|
|
66
|
+
# an action nobody manages) as rule.bypassed.
|
|
67
|
+
#
|
|
68
|
+
# @raise [PermissionForge::ActionNotAuthorizedError] when the current
|
|
69
|
+
# user may not run this action
|
|
70
|
+
def pf_authorize_action!
|
|
71
|
+
user = PermissionForge.current_user
|
|
72
|
+
decision = PermissionForge::ControllerActions::Authorizer.call(
|
|
73
|
+
controller_path: controller_path, action_name: action_name, controller: self, user: user
|
|
74
|
+
)
|
|
75
|
+
return pf_action_allowed(decision, user) if decision.allowed?
|
|
76
|
+
|
|
77
|
+
target = decision.target
|
|
78
|
+
pf_action_denied(decision, user)
|
|
79
|
+
raise PermissionForge::ActionNotAuthorizedError.new(
|
|
80
|
+
resource: target.resource, action: target.action, controller_path: controller_path,
|
|
81
|
+
reason: decision.reason, user: user
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def pf_action_allowed(decision, user)
|
|
86
|
+
reason = decision.bypass_reason
|
|
87
|
+
return unless reason
|
|
88
|
+
|
|
89
|
+
PermissionForge::Audit::Runtime.bypassed(reason, model: decision.target.resource, via: 'action_guard', user: user,
|
|
90
|
+
data: pf_action_event_data(decision))
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def pf_action_denied(decision, user)
|
|
94
|
+
PermissionForge::Audit::Runtime.denied(
|
|
95
|
+
PermissionForge::Audit::Runtime::ACTION_DENIED, reason: decision.reason, model: decision.target.resource,
|
|
96
|
+
via: 'action_guard', user: user,
|
|
97
|
+
data: pf_action_event_data(decision)
|
|
98
|
+
)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def pf_action_event_data(decision)
|
|
102
|
+
{ 'action' => decision.target.action, 'controller' => controller_path, 'action_name' => action_name }
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|