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,223 @@
|
|
|
1
|
+
require_relative 'gdpr_field_auditor/text_report'
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Services
|
|
5
|
+
# Reports fields that look like personal data and are neither masked nor mapped. It reports and never changes anything.
|
|
6
|
+
#
|
|
7
|
+
# @api public
|
|
8
|
+
class GdprFieldAuditor
|
|
9
|
+
# @param configuration [Configuration]
|
|
10
|
+
# @return [Array<Hash>] one entry per finding
|
|
11
|
+
def self.call(configuration: PermissionForge.configuration)
|
|
12
|
+
new(configuration: configuration).call
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(configuration: PermissionForge.configuration)
|
|
16
|
+
@configuration = configuration
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @return [Array<Hash>] one entry per finding
|
|
20
|
+
def call
|
|
21
|
+
report = []
|
|
22
|
+
audit_unmasked_fields(report)
|
|
23
|
+
audit_unmapped_columns(report)
|
|
24
|
+
audit_implicit_virtual_grants(report)
|
|
25
|
+
audit_view_all_on_pii_resources(report)
|
|
26
|
+
audit_cross_set_unmasking(report)
|
|
27
|
+
report
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
# Field rows that reach someone's permission hash: in effect, in a
|
|
33
|
+
# granting Permission Set. Every check reads rows through here, so a row
|
|
34
|
+
# the engine skips (switched off, or in a switched-off resource or set,
|
|
35
|
+
# or in a muting set) is never reported as both "unmasked" and "unmapped".
|
|
36
|
+
def granting_fields
|
|
37
|
+
PermissionForge::ResourceField.in_effect.merge(PermissionForge::PermissionSet.granting)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def audit_unmasked_fields(report)
|
|
41
|
+
granting_fields.where(mask_access: false).preload(resource: :permission_set).find_each do |field|
|
|
42
|
+
report << unmasked_finding(field) if gdpr_field?(field.name)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def unmasked_finding(field)
|
|
47
|
+
resource = field.resource
|
|
48
|
+
{ type: :unmasked_field, resource_field_id: field.id, field_name: field.name, resource_name: resource.name,
|
|
49
|
+
suggested_strategy: gdpr_strategy(field.name), permission_set: resource.permission_set.name }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def audit_unmapped_columns(report)
|
|
53
|
+
PermissionForge::Resource.distinct.pluck(:name).each do |resource_name|
|
|
54
|
+
audit_unmapped_for_resource(report, resource_name)
|
|
55
|
+
end
|
|
56
|
+
rescue ActiveRecord::StatementInvalid
|
|
57
|
+
# Tables don't exist yet (pre-migration)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def audit_unmapped_for_resource(report, resource_name)
|
|
61
|
+
columns = model_columns_for(resource_name)
|
|
62
|
+
return unless columns
|
|
63
|
+
|
|
64
|
+
virtual = unmapped_virtual_names_for(resource_name)
|
|
65
|
+
mapped_names = mapped_field_names_for(resource_name)
|
|
66
|
+
(columns + virtual - mapped_names).each do |column_name|
|
|
67
|
+
match = gdpr_patterns.find { |regex, _| column_name.match?(regex) }
|
|
68
|
+
next unless match
|
|
69
|
+
|
|
70
|
+
finding = { type: :unmapped_column, resource: resource_name, column: column_name, pattern_matched: match[1] }
|
|
71
|
+
report << (virtual.include?(column_name) ? finding.merge(virtual: true) : finding)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Virtual fields that follow unconfigured_field_strategy when nobody
|
|
76
|
+
# configured them. Implicitly granted ones are reported per Permission
|
|
77
|
+
# Set by audit_implicit_virtual_grants instead.
|
|
78
|
+
def unmapped_virtual_names_for(resource_name)
|
|
79
|
+
fields = virtual_fields(resource_name)
|
|
80
|
+
return [] unless fields
|
|
81
|
+
|
|
82
|
+
fields.names.reject { |name| fields.implicit_grant?(name) }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Declared virtual fields that look like personal data and have no active
|
|
86
|
+
# field row in a granting resource: under extra_fields_default_access =
|
|
87
|
+
# :grant that set gives its people read and edit on them, unmasked.
|
|
88
|
+
def audit_implicit_virtual_grants(report)
|
|
89
|
+
return unless @configuration.grant_extra_fields_by_default?
|
|
90
|
+
|
|
91
|
+
granting_resources.each { |resource| report.concat(implicit_virtual_grants_in(resource)) }
|
|
92
|
+
rescue ActiveRecord::StatementInvalid
|
|
93
|
+
# Tables don't exist yet (pre-migration)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# A switched-off row counts as no row, because the engine skips it.
|
|
97
|
+
def implicit_virtual_grants_in(resource)
|
|
98
|
+
configured = resource.resource_fields.select(&:is_active).map(&:name)
|
|
99
|
+
implicit_pii_fields(resource.name).filter_map do |name, strategy|
|
|
100
|
+
next if configured.include?(name)
|
|
101
|
+
|
|
102
|
+
{ type: :implicit_virtual_grant, resource_name: resource.name, field_name: name,
|
|
103
|
+
suggested_strategy: strategy, permission_set: resource.permission_set.name }
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def granting_resources
|
|
108
|
+
granting_sets = PermissionForge::PermissionSet.active.granting.select(:id)
|
|
109
|
+
PermissionForge::Resource.active.where(read_access: true, permission_set_id: granting_sets)
|
|
110
|
+
.includes(:permission_set, :resource_fields)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# [[name, suggested strategy]] for the model's implicitly granted names
|
|
114
|
+
# that match a GDPR pattern. Names in ignored_fields are left out: the
|
|
115
|
+
# admin does not manage those.
|
|
116
|
+
def implicit_pii_fields(resource_name)
|
|
117
|
+
@implicit_pii_fields ||= {}
|
|
118
|
+
@implicit_pii_fields[resource_name] ||= begin
|
|
119
|
+
names = Array(virtual_fields(resource_name)&.implicit).uniq - Array(@configuration.ignored_fields).map(&:to_s)
|
|
120
|
+
names.filter_map { |name| [name, gdpr_strategy(name)] if gdpr_field?(name) }
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# @return [PermissionForge::VirtualFields, nil] nil when the resource
|
|
125
|
+
# names no model whose columns can be read.
|
|
126
|
+
def virtual_fields(resource_name)
|
|
127
|
+
@virtual_fields ||= {}
|
|
128
|
+
@virtual_fields.fetch(resource_name) do
|
|
129
|
+
@virtual_fields[resource_name] = build_virtual_fields(resource_name)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def build_virtual_fields(resource_name)
|
|
134
|
+
return unless model_columns_for(resource_name)
|
|
135
|
+
|
|
136
|
+
PermissionForge::VirtualFields.new(resource_name.safe_constantize, @configuration)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def model_columns_for(resource_name)
|
|
140
|
+
model_class = resource_name.safe_constantize
|
|
141
|
+
return unless model_class
|
|
142
|
+
return unless model_class.respond_to?(:column_names)
|
|
143
|
+
|
|
144
|
+
model_class.column_names
|
|
145
|
+
rescue ActiveRecord::StatementInvalid
|
|
146
|
+
nil
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Names with a field row in effect in any granting Permission Set. A row
|
|
150
|
+
# the engine skips counts as no row.
|
|
151
|
+
def mapped_field_names_for(resource_name)
|
|
152
|
+
granting_fields.merge(PermissionForge::Resource.where(name: resource_name)).distinct.pluck(:name)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def audit_view_all_on_pii_resources(report)
|
|
156
|
+
resources_with_bypass.find_each do |resource|
|
|
157
|
+
pii_fields = resource.resource_fields.select { |field| gdpr_field?(field.name) }
|
|
158
|
+
next if pii_fields.empty?
|
|
159
|
+
|
|
160
|
+
report << build_view_all_finding(resource, pii_fields)
|
|
161
|
+
end
|
|
162
|
+
rescue ActiveRecord::StatementInvalid
|
|
163
|
+
# Tables don't exist yet (pre-migration)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Resources in effect in a granting Permission Set. In a muting set
|
|
167
|
+
# view_all takes the bypass away, and a switched-off row gives nothing.
|
|
168
|
+
def resources_with_bypass
|
|
169
|
+
resources = PermissionForge::Resource.arel_table
|
|
170
|
+
PermissionForge::Resource.in_effect.merge(PermissionForge::PermissionSet.granting)
|
|
171
|
+
.where(resources[:view_all].eq(true).or(resources[:modify_all].eq(true)))
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def build_view_all_finding(resource, pii_fields)
|
|
175
|
+
bypasses = []
|
|
176
|
+
bypasses << 'view_all' if resource.view_all
|
|
177
|
+
bypasses << 'modify_all' if resource.modify_all
|
|
178
|
+
|
|
179
|
+
{ type: :view_all_on_pii_resource, resource_name: resource.name,
|
|
180
|
+
permission_set: resource.permission_set.name, bypasses: bypasses,
|
|
181
|
+
pii_fields: pii_fields.map(&:name) }
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Masks combine with AND, so an unmasked row wins over a masked row of
|
|
185
|
+
# the same field of the same resource in another Permission Set.
|
|
186
|
+
def audit_cross_set_unmasking(report)
|
|
187
|
+
masking_sets = sets_masking_pii_fields
|
|
188
|
+
granting_fields.where(mask_access: false).preload(resource: :permission_set).find_each do |field|
|
|
189
|
+
resource = field.resource
|
|
190
|
+
masked_elsewhere = masking_sets.fetch([resource.name, field.name], []) - [resource.permission_set_id]
|
|
191
|
+
report << cross_set_finding(resource, field.name) if masked_elsewhere.any?
|
|
192
|
+
end
|
|
193
|
+
rescue ActiveRecord::StatementInvalid
|
|
194
|
+
# Tables don't exist yet (pre-migration)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def cross_set_finding(resource, field_name)
|
|
198
|
+
{ type: :cross_set_unmasking, field_name: field_name, resource_name: resource.name,
|
|
199
|
+
permission_set: resource.permission_set.name }
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# { [resource name, field name] => ids of the granting sets that mask it }
|
|
203
|
+
def sets_masking_pii_fields
|
|
204
|
+
masked = granting_fields.where(mask_access: true).preload(:resource).select { |field| gdpr_field?(field.name) }
|
|
205
|
+
masked.each_with_object({}) do |field, sets|
|
|
206
|
+
(sets[[field.resource.name, field.name]] ||= []) << field.resource.permission_set_id
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def gdpr_field?(name)
|
|
211
|
+
gdpr_patterns.any? { |regex, _| name.match?(regex) }
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def gdpr_strategy(name)
|
|
215
|
+
gdpr_patterns.find { |regex, _| name.match?(regex) }&.last
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def gdpr_patterns
|
|
219
|
+
@configuration.gdpr_field_patterns
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Services
|
|
5
|
+
# Refuses to read permissions or write assignments when a column that
|
|
6
|
+
# stores a host primary key cannot hold it.
|
|
7
|
+
#
|
|
8
|
+
# An integer column silently truncates a UUID or string key to its leading
|
|
9
|
+
# digits ('1aaa…' and '1bbb…' both become 1), so different users would
|
|
10
|
+
# share permission sets and different tenants would share permission data.
|
|
11
|
+
# The install generator types these columns from the host models; this
|
|
12
|
+
# guard covers installs where it could not, or that predate it.
|
|
13
|
+
#
|
|
14
|
+
# Checked lazily where the truncation would bite (PermissionCacheAccessor,
|
|
15
|
+
# the two assignment models) rather than at boot, so `db:migrate` can still
|
|
16
|
+
# run to repair an affected schema. A passing check is remembered until the
|
|
17
|
+
# configuration it depends on changes.
|
|
18
|
+
#
|
|
19
|
+
# @api private
|
|
20
|
+
class HostKeyGuard
|
|
21
|
+
Mismatch = Struct.new(:table, :column, :column_type, :host, :key_type) do
|
|
22
|
+
def to_s
|
|
23
|
+
"#{table}.#{column} is #{column_type} but #{host}'s primary key is #{key_type}"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class << self
|
|
28
|
+
# @return [true]
|
|
29
|
+
# @raise [PermissionForge::ConfigurationError] when a host key would be truncated
|
|
30
|
+
def verify!
|
|
31
|
+
signature = configuration_signature
|
|
32
|
+
return true if @verified == signature
|
|
33
|
+
|
|
34
|
+
found = mismatches
|
|
35
|
+
raise PermissionForge::ConfigurationError, message(found) if found.any?
|
|
36
|
+
|
|
37
|
+
@verified = signature
|
|
38
|
+
true
|
|
39
|
+
rescue ActiveRecord::StatementInvalid, ActiveRecord::NoDatabaseError, ActiveRecord::ConnectionNotEstablished
|
|
40
|
+
true # schema not readable yet (before the install migration); not remembered
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def reset!
|
|
44
|
+
@verified = nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @return [Array<Mismatch>] columns that would truncate the key they store
|
|
48
|
+
def mismatches
|
|
49
|
+
key_columns.filter_map { |model, column, host_name| mismatch(model, column, host_name) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def configuration_signature
|
|
55
|
+
config = PermissionForge.configuration
|
|
56
|
+
[config.user_class, config.table_name_prefix, config.tenant_model, config.tenant_column]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# [model, column, host class name] for every column holding a host key
|
|
60
|
+
# that an authorization decision depends on.
|
|
61
|
+
def key_columns
|
|
62
|
+
config = PermissionForge.configuration
|
|
63
|
+
user_class = config.user_class
|
|
64
|
+
columns = [[PermissionForge::PermissionSetUser, 'user_id', user_class],
|
|
65
|
+
[PermissionForge::PermissionSetGroupUser, 'user_id', user_class]]
|
|
66
|
+
return columns unless config.tenant_enabled?
|
|
67
|
+
|
|
68
|
+
tenant_column = config.resolved_tenant_column.to_s
|
|
69
|
+
columns + tenant_scoped_models.map { |model| [model, tenant_column, config.tenant_model] }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# The audit log is tenant-scoped as well (a truncated tenant id would
|
|
73
|
+
# show one tenant another's events); it is checked only once its table
|
|
74
|
+
# exists, so an install that has not run the upgrade yet keeps the
|
|
75
|
+
# rest of the check.
|
|
76
|
+
def tenant_scoped_models
|
|
77
|
+
models = [PermissionForge::PermissionSet, PermissionForge::PermissionSetUser,
|
|
78
|
+
PermissionForge::PermissionSetGroup, PermissionForge::PermissionSetGroupUser,
|
|
79
|
+
PermissionForge::Resource, PermissionForge::ResourceField,
|
|
80
|
+
PermissionForge::ResourceAction, PermissionForge::PermissionSetRule]
|
|
81
|
+
PermissionForge::AuditEvent.table_exists? ? models + [PermissionForge::AuditEvent] : models
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def mismatch(model, column, host_name)
|
|
85
|
+
host = host_name.to_s.safe_constantize
|
|
86
|
+
key_type = primary_key_type(host)
|
|
87
|
+
column_type = model.columns_hash[column]&.type
|
|
88
|
+
return unless column_type == :integer && key_type && key_type != :integer
|
|
89
|
+
|
|
90
|
+
Mismatch.new(model.table_name, column, column_type, host.name, key_type)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# nil for a missing model or a composite primary key (not checked).
|
|
94
|
+
def primary_key_type(host)
|
|
95
|
+
return unless host.respond_to?(:columns_hash)
|
|
96
|
+
|
|
97
|
+
key = host.primary_key
|
|
98
|
+
host.columns_hash[key]&.type if key.is_a?(String)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def message(found)
|
|
102
|
+
"PermissionForge columns cannot hold the host primary keys they store: #{found.join('; ')}. " \
|
|
103
|
+
'An integer column truncates a UUID or string key to its leading digits, so different users ' \
|
|
104
|
+
'(or tenants) would share permission data. Change these columns to the type of the key and ' \
|
|
105
|
+
're-create the affected rows; see "UUID or string keys" in docs/migration-guide.md.'
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
module PermissionForge
|
|
2
|
+
# The service objects hosts call (`Service.call(...)`): assignment, erasure, filtering, auditing and cloning.
|
|
3
|
+
# Each public service documents its arguments and result; helpers under this namespace are `@api private`.
|
|
4
|
+
#
|
|
5
|
+
# @api public
|
|
6
|
+
module Services
|
|
7
|
+
# WARNING: These strategies provide display-level obfuscation only.
|
|
8
|
+
# They do NOT constitute GDPR-compliant anonymization or pseudonymization.
|
|
9
|
+
# See docs/gdpr_masking_disclaimer.md for details.
|
|
10
|
+
#
|
|
11
|
+
# @api public
|
|
12
|
+
class MaskValue
|
|
13
|
+
BUILT_IN_STRATEGIES = {
|
|
14
|
+
'full' => :mask_full,
|
|
15
|
+
'email' => :mask_email,
|
|
16
|
+
'last4' => :mask_last4,
|
|
17
|
+
'first1_last4' => :mask_first1_last4
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
STRATEGIES = BUILT_IN_STRATEGIES.keys.freeze
|
|
21
|
+
MASK_CHAR = '*'.freeze
|
|
22
|
+
|
|
23
|
+
# @return [Hash{String => #call}] the built-in strategies merged with `config.custom_mask_strategies`
|
|
24
|
+
def self.strategies
|
|
25
|
+
custom = PermissionForge.configuration.custom_mask_strategies
|
|
26
|
+
return BUILT_IN_STRATEGIES if custom.blank?
|
|
27
|
+
|
|
28
|
+
BUILT_IN_STRATEGIES.merge(custom)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Masks a value with a built-in or custom strategy.
|
|
32
|
+
# @param value [Object]
|
|
33
|
+
# @param strategy [String] `full`, `email`, `last4`, `first1_last4` or a custom one
|
|
34
|
+
# @return [Object] the masked value (nil stays nil)
|
|
35
|
+
def self.call(value, strategy: 'full')
|
|
36
|
+
return value if value.nil?
|
|
37
|
+
|
|
38
|
+
str = value.to_s
|
|
39
|
+
return str if str.empty?
|
|
40
|
+
|
|
41
|
+
new(str, strategy).call
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def initialize(value, strategy)
|
|
45
|
+
@value = value
|
|
46
|
+
@strategy = strategy
|
|
47
|
+
all_strategies = self.class.strategies
|
|
48
|
+
return if all_strategies.key?(@strategy)
|
|
49
|
+
|
|
50
|
+
raise ArgumentError,
|
|
51
|
+
"Unknown mask strategy: #{strategy.inspect}. Valid strategies: #{all_strategies.keys.join(', ')}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @return [Object] the masked value
|
|
55
|
+
def call
|
|
56
|
+
all_strategies = self.class.strategies
|
|
57
|
+
strategy_ref = all_strategies[@strategy]
|
|
58
|
+
|
|
59
|
+
if strategy_ref.is_a?(Symbol)
|
|
60
|
+
send(strategy_ref)
|
|
61
|
+
else
|
|
62
|
+
strategy_ref.call(@value)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def mask_full
|
|
69
|
+
MASK_CHAR * 8
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def mask_email
|
|
73
|
+
return mask_full unless @value.include?('@')
|
|
74
|
+
|
|
75
|
+
local, domain = @value.split('@', 2)
|
|
76
|
+
masked_local = local.length > 1 ? "#{local[0]}#{MASK_CHAR * 3}" : MASK_CHAR
|
|
77
|
+
"#{masked_local}@#{domain}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def mask_last4
|
|
81
|
+
return mask_full if @value.length <= 4
|
|
82
|
+
|
|
83
|
+
"#{MASK_CHAR * (@value.length - 4)}#{@value[-4..]}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def mask_first1_last4
|
|
87
|
+
return mask_full if @value.length <= 5
|
|
88
|
+
|
|
89
|
+
"#{@value[0]}#{MASK_CHAR * (@value.length - 5)}#{@value[-4..]}"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Services
|
|
5
|
+
# Removes every cached permission hash — this version's entries, the
|
|
6
|
+
# previous versions' (Cache::UserKeys.namespaces), and the per-context
|
|
7
|
+
# variants — for every user, in every tenant. The step to run once the
|
|
8
|
+
# last process of the previous release is gone (0.14: an invalidation
|
|
9
|
+
# made by 0.13 cannot reach this version's entries, and one made after
|
|
10
|
+
# the clear would miss them again), and the way to revoke everything
|
|
11
|
+
# cached at once. Every user then rebuilds on their next request: a cold
|
|
12
|
+
# cache, one rebuild each. Two strategies, the first that works:
|
|
13
|
+
#
|
|
14
|
+
# :pattern — the store removes keys by pattern (Redis, memory and file
|
|
15
|
+
# stores: Cache::Base#clear_entries): one call per namespace, nothing
|
|
16
|
+
# enumerated. How many users or tenants that reached is not known
|
|
17
|
+
# (the result and the events say so). On a Redis store the
|
|
18
|
+
# invalidation stamps go with the bare keys — a rebuild writes a new
|
|
19
|
+
# one, one in flight is not kept.
|
|
20
|
+
# :enumerated — otherwise (Memcached, Solid Cache): every id of the
|
|
21
|
+
# user model, in slices of Cache::HolderInvalidation::SLICE, through
|
|
22
|
+
# the bulk invalidation (one stamp write and one removal per slice),
|
|
23
|
+
# or through each user's own invalidate_permissions where the bulk
|
|
24
|
+
# path must not be used (HolderInvalidation.by_id?: a host override,
|
|
25
|
+
# a listener, a snapshot transaction); then the per-context variants
|
|
26
|
+
# an invalidation leaves (Cache::VariantRemoval: one read of the
|
|
27
|
+
# slice's index entries, and a stamp write and a removal for the
|
|
28
|
+
# users who had a variant stored). With tenancy on: once per tenant of the
|
|
29
|
+
# tenant model, the users as the host scopes them under that tenant
|
|
30
|
+
# (a user model that is not tenant-scoped is visited under every
|
|
31
|
+
# tenant: users × tenants), and once more without a tenant for the
|
|
32
|
+
# tenant-agnostic keys and users of no tenant. A user a host default
|
|
33
|
+
# scope hides under a tenant keeps that tenant's key until cache_ttl.
|
|
34
|
+
#
|
|
35
|
+
# @example
|
|
36
|
+
# result = PermissionCacheClearer.call(actor: 'ops: TCK-12', reason: 'deploy 0.14')
|
|
37
|
+
# # => { strategy: :enumerated, users: 240, tenants: 2, unconfirmed: 0 }
|
|
38
|
+
# # => { strategy: :pattern, users: nil, tenants: 2, unconfirmed: 0 }
|
|
39
|
+
#
|
|
40
|
+
# `users` counts visits (a user once per tenant); `unconfirmed` the ones
|
|
41
|
+
# the cache store did not confirm (each also reported through
|
|
42
|
+
# `config.on_cache_failure`, and owed). The audit log: one `cache.cleared`
|
|
43
|
+
# event per tenant, in that tenant with that tenant's counts, plus one
|
|
44
|
+
# without a tenant with the totals (`across_tenants`); under :pattern
|
|
45
|
+
# the counts are not known and the events say `counted: false`.
|
|
46
|
+
# `actor:` and `reason:` as the other housekeeping services (rake:
|
|
47
|
+
# ACTOR=, REASON=).
|
|
48
|
+
#
|
|
49
|
+
# @api public
|
|
50
|
+
class PermissionCacheClearer
|
|
51
|
+
include Concerns::AuditedOperation
|
|
52
|
+
|
|
53
|
+
EVENT = 'cache.cleared'
|
|
54
|
+
SLICE = Cache::HolderInvalidation::SLICE
|
|
55
|
+
|
|
56
|
+
# Clears every cached permission hash (see the class comment).
|
|
57
|
+
#
|
|
58
|
+
# @param actor [Object, nil] who runs it, for the cache.cleared events
|
|
59
|
+
# @param reason [String, nil] why
|
|
60
|
+
# @return [Hash] strategy:, users:, tenants:, unconfirmed:
|
|
61
|
+
def self.call(...) = new(...).call
|
|
62
|
+
|
|
63
|
+
def initialize(actor: nil, reason: nil)
|
|
64
|
+
@actor = actor
|
|
65
|
+
@reason = reason
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @return [Hash] strategy:, users:, tenants:, unconfirmed:
|
|
69
|
+
def call
|
|
70
|
+
audited(actor: @actor, reason: @reason) { clear }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def clear
|
|
76
|
+
result = clear_by_pattern || clear_by_enumeration
|
|
77
|
+
# retire every memo in this process, as an invalidation does
|
|
78
|
+
PermissionForge.bump_permissions_cache_version!
|
|
79
|
+
result
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @return [Hash, nil] nil when the store cannot remove by pattern
|
|
83
|
+
def clear_by_pattern
|
|
84
|
+
cache = PermissionForge.cache
|
|
85
|
+
return unless Cache::UserKeys.namespaces.all? { |namespace| cache.clear_entries(namespace) }
|
|
86
|
+
|
|
87
|
+
tenant_ids = tenants.map(&:id)
|
|
88
|
+
tenant_ids.each { |tenant_id| record(tenant_id, strategy: :pattern, counted: false, unconfirmed: 0) }
|
|
89
|
+
record_without_tenant(strategy: :pattern, counted: false, unconfirmed: 0, tenants: tenant_ids.size)
|
|
90
|
+
{ strategy: :pattern, users: nil, tenants: tenant_ids.size, unconfirmed: 0 }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def clear_by_enumeration
|
|
94
|
+
totals = { strategy: :enumerated, users: 0, tenants: 0, unconfirmed: 0 }
|
|
95
|
+
tenants.each { |tenant| add_counts(totals, clear_tenant(tenant), tenants: 1) }
|
|
96
|
+
add_counts(totals, without_tenant { invalidate_everyone(nil) })
|
|
97
|
+
record_without_tenant(**totals)
|
|
98
|
+
totals
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# One tenant's users, under that tenant, and its event.
|
|
102
|
+
def clear_tenant(tenant)
|
|
103
|
+
counts = PermissionForge.with_ambient_tenant(tenant) { invalidate_everyone(tenant.id) }
|
|
104
|
+
record(tenant.id, strategy: :enumerated, **counts)
|
|
105
|
+
counts
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def add_counts(totals, counts, **more)
|
|
109
|
+
totals.merge!(counts, more) { |_key, total, count| total + count }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Every user the model lists here, in slices, by id where that is
|
|
113
|
+
# allowed and through each user's invalidate_permissions where not.
|
|
114
|
+
#
|
|
115
|
+
# @return [Hash] users:, unconfirmed:
|
|
116
|
+
def invalidate_everyone(tenant_id)
|
|
117
|
+
model = PermissionForge.configuration.user_model
|
|
118
|
+
by_id = Cache::HolderInvalidation.by_id?(model)
|
|
119
|
+
counts = { users: 0, unconfirmed: 0 }
|
|
120
|
+
users_scope(model, tenant_id).in_batches(of: SLICE) do |batch|
|
|
121
|
+
ids = batch.pluck(model.primary_key)
|
|
122
|
+
counts[:users] += ids.size
|
|
123
|
+
counts[:unconfirmed] += by_id ? by_id(ids, tenant_id).size : one_by_one(batch, tenant_id)
|
|
124
|
+
end
|
|
125
|
+
counts
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# The invalidation leaves the variants to their TTL; they hold the
|
|
129
|
+
# profile, so the clear removes them too.
|
|
130
|
+
#
|
|
131
|
+
# @return [Array] the ids the store did not confirm
|
|
132
|
+
def by_id(ids, tenant_id)
|
|
133
|
+
Cache::HolderInvalidation.call(user_ids: ids, tenant_id: tenant_id) | Cache::VariantRemoval.call(user_ids: ids, tenant_id: tenant_id)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# @return [Integer] how many the store did not confirm
|
|
137
|
+
def one_by_one(batch, tenant_id)
|
|
138
|
+
batch.reject do |user|
|
|
139
|
+
Cache::Failure.none? do
|
|
140
|
+
user.invalidate_permissions(tenant_id: tenant_id)
|
|
141
|
+
Cache::VariantRemoval.call(user_ids: [user.id], tenant_id: tenant_id, ambient_key: user.permission_cache_key)
|
|
142
|
+
end
|
|
143
|
+
end.size
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Under a tenant: the users as the host scopes them (a tenant-scoped
|
|
147
|
+
# user model lists that tenant's). Without one: everyone.
|
|
148
|
+
def users_scope(model, tenant_id)
|
|
149
|
+
PermissionForge::Concerns::AutoPermissionScoping.internally_suspended { tenant_id ? model.all : model.unscoped }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# The tenants of the tenant model, when tenancy is on; none otherwise.
|
|
153
|
+
def tenants
|
|
154
|
+
return [] unless PermissionForge.configuration.tenant_enabled?
|
|
155
|
+
|
|
156
|
+
@tenants ||= PermissionForge.configuration.tenant_model.constantize.unscoped.to_a
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def without_tenant(&)
|
|
160
|
+
defined?(ActsAsTenant) ? ActsAsTenant.without_tenant(&) : yield
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def record(tenant_id, **data)
|
|
164
|
+
PermissionForge::Audit.record(EVENT, tenant_id: tenant_id,
|
|
165
|
+
data: data.transform_keys(&:to_s).merge('operation' => 'cache_clear'))
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# The event without a tenant — recorded with none ambient, so it is one.
|
|
169
|
+
def record_without_tenant(**data)
|
|
170
|
+
without_tenant { record(nil, **data, across_tenants: true) }
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|