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,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Services
|
|
5
|
+
module Concerns
|
|
6
|
+
# Runs a housekeeping service under an audit context: who ran it (`actor:`),
|
|
7
|
+
# why (`reason:`), from where, and one correlation id for the per-row events
|
|
8
|
+
# and the summary event it writes.
|
|
9
|
+
#
|
|
10
|
+
# `actor:` is a host user, an Audit::Actor, or a label String (stored as a
|
|
11
|
+
# 'system' actor). nil names nobody: the context already in force (a request's
|
|
12
|
+
# admin, a rake task's ACTOR, PermissionForge.current_user) stays — an explicit
|
|
13
|
+
# nil never replaces a real actor.
|
|
14
|
+
#
|
|
15
|
+
# The source is 'service' unless the caller's context, or the process (rake,
|
|
16
|
+
# console), already says where the call came from.
|
|
17
|
+
module AuditedOperation
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def audited(actor:, reason:, &)
|
|
21
|
+
PermissionForge::Audit::Context.with(actor: audit_actor_for(actor), source: audit_source, reason: reason) do
|
|
22
|
+
PermissionForge::Audit.correlate(&)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @return [PermissionForge::Audit::Actor, nil]
|
|
27
|
+
def audit_actor_for(actor)
|
|
28
|
+
case actor
|
|
29
|
+
when nil then nil
|
|
30
|
+
when String then PermissionForge::Audit::Actor.system(actor)
|
|
31
|
+
else PermissionForge::Audit::Actor.coerce(actor)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def audit_source
|
|
36
|
+
return if PermissionForge::Audit::Context[:source]
|
|
37
|
+
|
|
38
|
+
'service' if PermissionForge::Audit::Source.detect == 'service'
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module PermissionForge
|
|
2
|
+
module Services
|
|
3
|
+
# Copies one record as a new row. The copy is a new record in every sense:
|
|
4
|
+
# its own uuid, and no author or editor carried over from the original —
|
|
5
|
+
# HasAuditTrail credits whoever makes the copy, and the copy's create
|
|
6
|
+
# event (RecordsAuditEvents) names the same person. A field's "This is
|
|
7
|
+
# intended" acknowledgement is one person's attestation about one set, so
|
|
8
|
+
# a copy starts unacknowledged.
|
|
9
|
+
#
|
|
10
|
+
# @api private
|
|
11
|
+
module Clonable
|
|
12
|
+
# Columns a copy never takes from its original (those the table has).
|
|
13
|
+
RESET_ON_COPY = %w[uuid created_by_id updated_by_id sensitive_acknowledged_at sensitive_acknowledged_by_id].freeze
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def duplicate_record(record, **attrs)
|
|
18
|
+
cloned = record.dup
|
|
19
|
+
cloned.assign_attributes(reset_on_copy(cloned).merge(attrs))
|
|
20
|
+
cloned.save!
|
|
21
|
+
cloned
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def reset_on_copy(record)
|
|
25
|
+
RESET_ON_COPY.select { |column| record.has_attribute?(column) }.index_with(nil)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
module PermissionForge
|
|
2
|
+
module Services
|
|
3
|
+
# Handles consent withdrawal by removing a user from all permission sets
|
|
4
|
+
# gated on the specified consent flag, then invalidating the user's cached
|
|
5
|
+
# permissions (rebuilt read-through on next access).
|
|
6
|
+
#
|
|
7
|
+
# @example Withdraw marketing consent
|
|
8
|
+
# result = ConsentWithdrawalHandler.call(
|
|
9
|
+
# user: user,
|
|
10
|
+
# consent_flag: 'marketing_consent'
|
|
11
|
+
# )
|
|
12
|
+
# # => { revoked_sets: 2, invalidated: true }
|
|
13
|
+
#
|
|
14
|
+
# `invalidated` is false when nothing was revoked, and when the cache
|
|
15
|
+
# store did not confirm the invalidation (reported through
|
|
16
|
+
# `config.on_cache_failure`).
|
|
17
|
+
#
|
|
18
|
+
# @example Dry run (inspect without revoking)
|
|
19
|
+
# result = ConsentWithdrawalHandler.call(
|
|
20
|
+
# user: user,
|
|
21
|
+
# consent_flag: 'marketing_consent',
|
|
22
|
+
# dry_run: true
|
|
23
|
+
# )
|
|
24
|
+
# # => { revoked_sets: 0, affected_sets: ["Marketing Access"], dry_run: true }
|
|
25
|
+
#
|
|
26
|
+
# The audit log: assignments are destroyed through their callbacks (one
|
|
27
|
+
# `assignment.revoked` each), then ONE `consent.withdrawn` event names the
|
|
28
|
+
# person, the flag, the sets and the counts. `actor:` (a user, an
|
|
29
|
+
# Audit::Actor or a label String) and `reason:` say who ran it and why.
|
|
30
|
+
# A dry run writes nothing.
|
|
31
|
+
#
|
|
32
|
+
# The revocations and `consent.withdrawn` happen in ONE transaction, like
|
|
33
|
+
# DataSubjectErasure: if the summary cannot be written under
|
|
34
|
+
# audit_change_failure_strategy :raise, no assignment is removed. The
|
|
35
|
+
# cache invalidation runs after the commit (so the summary does not say
|
|
36
|
+
# whether the store confirmed it; the result's `invalidated` does).
|
|
37
|
+
#
|
|
38
|
+
# Tenants: with a tenant ambient, the person's assignments in that
|
|
39
|
+
# tenant. With none (a platform task) in every tenant — also under
|
|
40
|
+
# acts_as_tenant's require_tenant — and `consent.withdrawn` is written in
|
|
41
|
+
# each tenant that lost assignments (its sets and count) plus once
|
|
42
|
+
# without a tenant (the totals), each with `across_tenants: true`
|
|
43
|
+
# (Concerns::AcrossTenants).
|
|
44
|
+
#
|
|
45
|
+
# @api public
|
|
46
|
+
class ConsentWithdrawalHandler
|
|
47
|
+
include Concerns::AuditedOperation
|
|
48
|
+
include Concerns::AcrossTenants
|
|
49
|
+
|
|
50
|
+
# Removes a user from every Permission Set gated on the consent flag.
|
|
51
|
+
# @return [Hash] see {#call}
|
|
52
|
+
def self.call(...) = new(...).call
|
|
53
|
+
|
|
54
|
+
def initialize(user:, consent_flag:, dry_run: false, actor: nil, reason: nil)
|
|
55
|
+
@user = user
|
|
56
|
+
@consent_flag = consent_flag
|
|
57
|
+
@dry_run = dry_run
|
|
58
|
+
@actor = actor
|
|
59
|
+
@reason = reason
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @return [Hash] `{ revoked_sets:, invalidated: }` (plus `affected_sets:` and `dry_run:` for a dry run)
|
|
63
|
+
def call
|
|
64
|
+
validate!
|
|
65
|
+
|
|
66
|
+
across_tenants do
|
|
67
|
+
affected_sets = consent_gated_sets
|
|
68
|
+
affected_assignments = user_assignments_for(affected_sets)
|
|
69
|
+
next dry_run_result(affected_sets, affected_assignments) if @dry_run
|
|
70
|
+
|
|
71
|
+
audited(actor: @actor, reason: @reason) { withdraw(affected_assignments) }
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def withdraw(affected_assignments)
|
|
78
|
+
revoked_count = ActiveRecord::Base.transaction { revoke_and_record(affected_assignments) }
|
|
79
|
+
# what the store confirmed — not what a host's override of the method returns
|
|
80
|
+
invalidated = revoked_count.positive? && PermissionForge::Cache::Failure.none? { @user.invalidate_permissions }
|
|
81
|
+
|
|
82
|
+
{ revoked_sets: revoked_count, invalidated: invalidated }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def revoke_and_record(affected_assignments)
|
|
86
|
+
assignments = affected_assignments.includes(:permission_set).to_a
|
|
87
|
+
revoked = assignments.select { |assignment| assignment.destroy.present? }
|
|
88
|
+
record_summary(assignments, revoked)
|
|
89
|
+
revoked.size
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# One event, or one per tenant plus the totals (record_per_tenant).
|
|
93
|
+
# Each names the sets of its assignments and counts the revoked ones.
|
|
94
|
+
def record_summary(assignments, revoked)
|
|
95
|
+
record_per_tenant('consent.withdrawn', assignments, subject_user_id: @user.id) do |rows|
|
|
96
|
+
summary_data(rows.filter_map(&:permission_set), rows.count { |row| revoked.include?(row) })
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def summary_data(sets, revoked_count)
|
|
101
|
+
{
|
|
102
|
+
'operation' => 'consent_withdrawal',
|
|
103
|
+
'consent_flag' => @consent_flag.to_s,
|
|
104
|
+
'subject_user_id' => @user.id.to_s,
|
|
105
|
+
'set_uuids' => sets.map(&:uuid).uniq,
|
|
106
|
+
'set_names' => sets.map(&:name).uniq,
|
|
107
|
+
'revoked' => revoked_count,
|
|
108
|
+
'dry_run' => false
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def validate!
|
|
113
|
+
raise ArgumentError, 'user is required' if @user.nil?
|
|
114
|
+
raise ArgumentError, 'consent_flag is required' if @consent_flag.blank?
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def consent_gated_sets
|
|
118
|
+
PermissionForge::PermissionSet.where(requires_session_flag: @consent_flag)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def user_assignments_for(sets)
|
|
122
|
+
PermissionForge::PermissionSetUser.where(user: @user, permission_set: sets)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def dry_run_result(sets, assignments)
|
|
126
|
+
{
|
|
127
|
+
revoked_sets: 0,
|
|
128
|
+
affected_sets: sets.pluck(:name),
|
|
129
|
+
affected_assignment_count: assignments.count,
|
|
130
|
+
dry_run: true
|
|
131
|
+
}
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module PermissionForge
|
|
2
|
+
module Services
|
|
3
|
+
# Aggregates dashboard statistics from permission models.
|
|
4
|
+
# Extracted from DashboardController for SRP.
|
|
5
|
+
#
|
|
6
|
+
# @api private
|
|
7
|
+
class DashboardStats
|
|
8
|
+
def self.call
|
|
9
|
+
{}.merge(permission_set_stats).merge(model_counts).merge(recent_records)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def permission_set_stats
|
|
16
|
+
{
|
|
17
|
+
permission_sets_count: PermissionForge::PermissionSet.count,
|
|
18
|
+
active_sets_count: PermissionForge::PermissionSet.active.count,
|
|
19
|
+
muting_sets_count: PermissionForge::PermissionSet.muting.count,
|
|
20
|
+
conditional_sets_count: PermissionForge::PermissionSet.conditional.count,
|
|
21
|
+
expired_assignments_count: PermissionForge::PermissionSetUser.expired.count
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def model_counts
|
|
26
|
+
{
|
|
27
|
+
resources_count: PermissionForge::Resource.count,
|
|
28
|
+
rules_count: PermissionForge::PermissionSetRule.count,
|
|
29
|
+
groups_count: PermissionForge::PermissionSetGroup.count,
|
|
30
|
+
users_count: PermissionForge::PermissionSetUser.select(:user_id).distinct.count
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def recent_records
|
|
35
|
+
{
|
|
36
|
+
recent_permission_sets: PermissionForge::PermissionSet.order(created_at: :desc).limit(5),
|
|
37
|
+
recent_resources: PermissionForge::Resource.order(created_at: :desc).limit(5)
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
module PermissionForge
|
|
2
|
+
module Services
|
|
3
|
+
# Supports a GDPR Art. 17 (right to erasure) request for a host user.
|
|
4
|
+
# What it removes:
|
|
5
|
+
# - the person's permission set assignments and group memberships, in
|
|
6
|
+
# the ambient tenant, each through its callbacks (one audit event per
|
|
7
|
+
# row);
|
|
8
|
+
# - the person's cached permissions;
|
|
9
|
+
# - the person's name labels in the audit log (actor_label on events
|
|
10
|
+
# they acted in, record_label on events about them): with a tenant
|
|
11
|
+
# current, in that tenant's events; with none (the rake task, a
|
|
12
|
+
# platform operator), in every tenant's, events without a tenant
|
|
13
|
+
# included;
|
|
14
|
+
# - whatever the host's `on_erasure` callback deletes.
|
|
15
|
+
# What remains, so the log still says what happened:
|
|
16
|
+
# - every audit event keeps its ids (actor_id, subject_user_id), ip,
|
|
17
|
+
# request_id, reason and data (snapshots of the removed rows);
|
|
18
|
+
# - created_by_id / updated_by_id / reviewed_by_id /
|
|
19
|
+
# sensitive_acknowledged_by_id on rows the person created, edited or
|
|
20
|
+
# reviewed;
|
|
21
|
+
# - assignments and memberships in other tenants when a tenant is
|
|
22
|
+
# current (counted in `assignments_remaining_elsewhere` and
|
|
23
|
+
# `group_assignments_remaining_elsewhere`);
|
|
24
|
+
# - the person's own record and data in the host app, unless
|
|
25
|
+
# `on_erasure` deletes them.
|
|
26
|
+
#
|
|
27
|
+
# @example Full erasure
|
|
28
|
+
# result = DataSubjectErasure.call(user: user)
|
|
29
|
+
# # => { assignments_removed: 3, group_assignments_removed: 1, cache_cleared: true, callback_invoked: true }
|
|
30
|
+
#
|
|
31
|
+
# `cache_cleared` is false when the cache store did not confirm the
|
|
32
|
+
# removal (it is unreachable, say): the assignments are gone, but the
|
|
33
|
+
# user's cached permissions may live until cache_ttl. The failure is also
|
|
34
|
+
# reported through `config.on_cache_failure`. It is nil when the service
|
|
35
|
+
# is called inside a database transaction: the assignments are only gone
|
|
36
|
+
# for everybody else — and their cached permissions with them — once that
|
|
37
|
+
# transaction has committed, and a failure then is reported through
|
|
38
|
+
# `config.on_cache_failure` alone.
|
|
39
|
+
#
|
|
40
|
+
# @example Dry run (inspect without removing)
|
|
41
|
+
# result = DataSubjectErasure.call(user: user, dry_run: true)
|
|
42
|
+
# # => { assignments_count: 3, group_assignments_count: 1, permission_sets: [...], dry_run: true }
|
|
43
|
+
#
|
|
44
|
+
# The audit log (docs/audit-log.md): the rows are destroyed through their
|
|
45
|
+
# callbacks, so each assignment writes its own `assignment.revoked`
|
|
46
|
+
# event; then the personal labels the log holds about the person are
|
|
47
|
+
# cleared and ONE `erasure.performed` event is written. `actor:` (a user,
|
|
48
|
+
# an Audit::Actor or a label String; nil means the actor of the context)
|
|
49
|
+
# and `reason:` say who ran it and why. A dry run writes nothing at all.
|
|
50
|
+
#
|
|
51
|
+
# The row removal, the label scrub and `erasure.performed` happen in ONE
|
|
52
|
+
# transaction: if the summary cannot be written under
|
|
53
|
+
# audit_change_failure_strategy :raise, nothing changed. Under :report or
|
|
54
|
+
# :log the host chose that changes go through when their record fails: the
|
|
55
|
+
# erasure completes without `erasure.performed` (the failure is reported) —
|
|
56
|
+
# the right to erasure is not blocked by the audit write. The cache
|
|
57
|
+
# invalidation and `on_erasure` run after it. Events a callback writes
|
|
58
|
+
# afterwards keep the labels they were given, so a host callback must not
|
|
59
|
+
# create assignments for the erased person.
|
|
60
|
+
#
|
|
61
|
+
# `on_erasure` runs once the erasure is committed for everybody:
|
|
62
|
+
# - called outside a transaction, right after the erasure; if it
|
|
63
|
+
# raises, the erasure stands and is recorded, and the error propagates
|
|
64
|
+
# (`callback_invoked` is true);
|
|
65
|
+
# - called inside the caller's transaction, after that transaction has
|
|
66
|
+
# committed (OpenTransaction#after_commit), and never if it rolls
|
|
67
|
+
# back (`callback_invoked` is nil: only known at the commit). An
|
|
68
|
+
# error there is logged and reported (Rails.error, `handled: true`,
|
|
69
|
+
# like every after-commit error the gem catches), not raised: raising
|
|
70
|
+
# from Rails' commit would skip the after-commit work queued behind
|
|
71
|
+
# it, the cache invalidations of the caller's other changes included.
|
|
72
|
+
#
|
|
73
|
+
# With no tenant ambient the service runs over every tenant also under
|
|
74
|
+
# acts_as_tenant's require_tenant (Concerns::AcrossTenants).
|
|
75
|
+
#
|
|
76
|
+
# @api public
|
|
77
|
+
class DataSubjectErasure
|
|
78
|
+
include Concerns::AuditedOperation
|
|
79
|
+
include Concerns::AcrossTenants
|
|
80
|
+
|
|
81
|
+
# Erases a user's permission data (GDPR Art. 17).
|
|
82
|
+
# @return [Hash] see {#call}
|
|
83
|
+
def self.call(...) = new(...).call
|
|
84
|
+
|
|
85
|
+
def initialize(user:, on_erasure: nil, dry_run: false, actor: nil, reason: nil)
|
|
86
|
+
@user = user
|
|
87
|
+
@on_erasure = on_erasure
|
|
88
|
+
@dry_run = dry_run
|
|
89
|
+
@actor = actor
|
|
90
|
+
@reason = reason
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# @return [Hash] the counts of what was removed and what remains
|
|
94
|
+
def call
|
|
95
|
+
validate!
|
|
96
|
+
|
|
97
|
+
across_tenants do
|
|
98
|
+
next dry_run_result if @dry_run
|
|
99
|
+
|
|
100
|
+
audited(actor: erased_person_unnamed, reason: @reason) { erase }
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def erase
|
|
107
|
+
removed = ActiveRecord::Base.transaction { remove_and_record }
|
|
108
|
+
cache_cleared = clear_cache
|
|
109
|
+
|
|
110
|
+
removed.merge(cache_cleared: cache_cleared, callback_invoked: run_callback)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def remove_and_record
|
|
114
|
+
sets = permission_set_assignments.includes(:permission_set).filter_map(&:permission_set)
|
|
115
|
+
groups = group_assignments.includes(:permission_set_group).filter_map(&:permission_set_group)
|
|
116
|
+
|
|
117
|
+
counts = { assignments_removed: remove_permission_set_assignments,
|
|
118
|
+
group_assignments_removed: remove_group_assignments }
|
|
119
|
+
counts.merge!(remaining_elsewhere)
|
|
120
|
+
scrubbed = scrub_labels
|
|
121
|
+
record_summary(sets, groups, counts, scrubbed)
|
|
122
|
+
counts
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# What the person still has in other tenants (nothing is removed there).
|
|
126
|
+
def remaining_elsewhere
|
|
127
|
+
{
|
|
128
|
+
assignments_remaining_elsewhere: PermissionForge::PermissionSetUser.unscoped.where(user: @user).count,
|
|
129
|
+
group_assignments_remaining_elsewhere: PermissionForge::PermissionSetGroupUser.unscoped.where(user: @user).count
|
|
130
|
+
}
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# A person who erases themselves must not leave their own name on the
|
|
134
|
+
# events of the erasure: the id stays, the label does not. Without
|
|
135
|
+
# `actor:` the context's actor (the admin of the request) is the one
|
|
136
|
+
# compared.
|
|
137
|
+
def erased_person_unnamed
|
|
138
|
+
resolved = audit_actor_for(@actor) || PermissionForge::Audit::Actor.current
|
|
139
|
+
return @actor unless resolved.type == 'user' && resolved.id == @user.id.to_s
|
|
140
|
+
|
|
141
|
+
PermissionForge::Audit::Actor.new(type: resolved.type, id: resolved.id)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# The only sanctioned mutation of audit events: the snapshots of the
|
|
145
|
+
# person's name. Every id stays, so the trail still says WHAT happened;
|
|
146
|
+
# only WHO (as a name) is forgotten. Past the insert-only guards on
|
|
147
|
+
# purpose (`update_all` is the one path T1 leaves open), in the events
|
|
148
|
+
# of scrubbed_events, only for actors of type 'user' (an API key or a
|
|
149
|
+
# basic auth login that shares the id is somebody else), and recorded
|
|
150
|
+
# in `erasure.performed`.
|
|
151
|
+
def scrub_labels
|
|
152
|
+
events = scrubbed_events
|
|
153
|
+
id = @user.id.to_s
|
|
154
|
+
{
|
|
155
|
+
actor_labels: events.where(actor_type: 'user', actor_id: id).where.not(actor_label: nil).update_all(actor_label: nil),
|
|
156
|
+
record_labels: events.where(subject_user_id: id).where.not(record_label: nil).update_all(record_label: nil)
|
|
157
|
+
}
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# With a tenant current (a tenant admin), that tenant's events only: a
|
|
161
|
+
# tenant cannot reach another tenant's log. With none (the rake task, a
|
|
162
|
+
# platform operator), every tenant's, and the events without a tenant.
|
|
163
|
+
# Built on `unscoped` and an explicit tenant condition, so it does not
|
|
164
|
+
# depend on acts_as_tenant's default scope. An events table without the
|
|
165
|
+
# tenant column cannot be told apart by tenant: every event.
|
|
166
|
+
def scrubbed_events
|
|
167
|
+
PermissionForge::AuditEvent.unscoped.in_tenant(PermissionForge.ambient_tenant_id)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def record_summary(sets, groups, counts, scrubbed)
|
|
171
|
+
PermissionForge::Audit.record('erasure.performed', subject_user_id: @user.id,
|
|
172
|
+
data: summary_data(sets, groups, counts, scrubbed))
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def summary_data(sets, groups, counts, scrubbed)
|
|
176
|
+
cleared = { 'events_scrubbed' => scrubbed.values.sum, 'actor_labels_cleared' => scrubbed[:actor_labels],
|
|
177
|
+
'record_labels_cleared' => scrubbed[:record_labels] }
|
|
178
|
+
names(sets, 'set').merge(names(groups, 'group'), counts.transform_keys(&:to_s), cleared,
|
|
179
|
+
'operation' => 'erasure', 'subject_user_id' => @user.id.to_s,
|
|
180
|
+
'callback_invoked' => !@on_erasure.nil?, 'dry_run' => false)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def names(records, kind)
|
|
184
|
+
{ "#{kind}_uuids" => records.map(&:uuid).uniq, "#{kind}_names" => records.map(&:name).uniq }
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def validate!
|
|
188
|
+
raise ArgumentError, 'user is required' if @user.nil?
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def permission_set_assignments
|
|
192
|
+
PermissionForge::PermissionSetUser.where(user: @user)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def group_assignments
|
|
196
|
+
PermissionForge::PermissionSetGroupUser.where(user: @user)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def remove_permission_set_assignments
|
|
200
|
+
records = permission_set_assignments
|
|
201
|
+
count = records.count
|
|
202
|
+
records.destroy_all
|
|
203
|
+
count
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def remove_group_assignments
|
|
207
|
+
records = group_assignments
|
|
208
|
+
count = records.count
|
|
209
|
+
records.destroy_all
|
|
210
|
+
count
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Asks the gem's own failure bookkeeping, not the value of
|
|
214
|
+
# invalidate_permissions: a host app overrides that method.
|
|
215
|
+
#
|
|
216
|
+
# An invalidation leaves the per-context variants of a user with
|
|
217
|
+
# conditional sets to their TTL (unreachable). They hold the profile,
|
|
218
|
+
# so they are removed as well (Cache::VariantRemoval) — inside the
|
|
219
|
+
# caller's transaction once it has committed, after the assignments'
|
|
220
|
+
# own invalidations: until then another request may still cache one
|
|
221
|
+
# from the rows as they were.
|
|
222
|
+
#
|
|
223
|
+
# @return [Boolean, nil] whether the store confirmed the removal; nil
|
|
224
|
+
# inside an open transaction, where that is only known at its commit
|
|
225
|
+
def clear_cache
|
|
226
|
+
transaction = PermissionForge::OpenTransaction.current
|
|
227
|
+
cleared = PermissionForge::Cache::Failure.none? do
|
|
228
|
+
invalidate_user
|
|
229
|
+
PermissionForge::Cache::VariantRemoval.call(**variant_removal) unless transaction
|
|
230
|
+
end
|
|
231
|
+
return cleared unless transaction
|
|
232
|
+
|
|
233
|
+
remove_variants_after_commit(transaction)
|
|
234
|
+
nil
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# The arguments of the person's Cache::VariantRemoval: the keys the
|
|
238
|
+
# instance names, as invalidate_permissions removes them, every slot of
|
|
239
|
+
# each without reading the index entries (a read that fails silently
|
|
240
|
+
# and an evicted index entry must not leave a variant behind).
|
|
241
|
+
def variant_removal
|
|
242
|
+
named = @user.respond_to?(:permission_cache_key) ? { ambient_key: @user.permission_cache_key } : {}
|
|
243
|
+
{ user_ids: [@user.id], indexed_only: false, **named }
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Nothing raises out of the commit: a failure is reported, like every
|
|
247
|
+
# after-commit invalidation (Cache::Failure).
|
|
248
|
+
def remove_variants_after_commit(transaction)
|
|
249
|
+
removal = variant_removal
|
|
250
|
+
transaction.after_commit do
|
|
251
|
+
PermissionForge::Cache::VariantRemoval.call(**removal)
|
|
252
|
+
rescue StandardError => e
|
|
253
|
+
PermissionForge::Cache::Failure.report(operation: :invalidate, user_id: removal[:user_ids].first, error: e)
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def invalidate_user
|
|
258
|
+
# invalidate_permissions (not a raw cache delete) so the ambient and
|
|
259
|
+
# tenant-agnostic keys are both removed AND the process-wide memo
|
|
260
|
+
# version bumps — no live instance may keep serving an erased user's
|
|
261
|
+
# permissions from its instance memo.
|
|
262
|
+
return @user.invalidate_permissions if @user.respond_to?(:invalidate_permissions)
|
|
263
|
+
|
|
264
|
+
cache = PermissionForge.cache
|
|
265
|
+
key = @user.permission_cache_key
|
|
266
|
+
PermissionForge::Cache::Failure.report(operation: :invalidate, keys: [key]) unless cache.expunge(key)
|
|
267
|
+
PermissionForge.bump_permissions_cache_version!
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# @return [Boolean, nil] false without a callback; true once it ran;
|
|
271
|
+
# nil when it waits for the caller's transaction to commit
|
|
272
|
+
def run_callback
|
|
273
|
+
return false if @on_erasure.nil?
|
|
274
|
+
|
|
275
|
+
transaction = PermissionForge::OpenTransaction.current
|
|
276
|
+
return defer_callback(transaction) if transaction
|
|
277
|
+
|
|
278
|
+
@on_erasure.call(@user)
|
|
279
|
+
true
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def defer_callback(transaction)
|
|
283
|
+
callback = @on_erasure
|
|
284
|
+
user = @user
|
|
285
|
+
transaction.after_commit { report_callback_failure { callback.call(user) } }
|
|
286
|
+
nil
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def report_callback_failure
|
|
290
|
+
yield
|
|
291
|
+
rescue StandardError => e
|
|
292
|
+
Rails.logger&.error("[PermissionForge] on_erasure failed after the erasure committed — #{e.class}: #{e.message}")
|
|
293
|
+
Rails.error.report(e, handled: true, source: 'permission_forge.data_subject_erasure')
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def dry_run_result
|
|
297
|
+
{
|
|
298
|
+
assignments_count: permission_set_assignments.count,
|
|
299
|
+
group_assignments_count: group_assignments.count,
|
|
300
|
+
permission_sets: permission_set_assignments.includes(:permission_set).filter_map { |psu| psu.permission_set&.name },
|
|
301
|
+
dry_run: true
|
|
302
|
+
}
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|