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,152 @@
|
|
|
1
|
+
module PermissionForge
|
|
2
|
+
module Services
|
|
3
|
+
# Removes expired PermissionSetUser records and invalidates the cached
|
|
4
|
+
# permissions of all affected users (rebuilt read-through on next access).
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# result = ExpiredAssignmentCleaner.call
|
|
8
|
+
# # => { removed: 5, invalidated: 3 }
|
|
9
|
+
#
|
|
10
|
+
# `invalidated` counts the users whose invalidation the cache store
|
|
11
|
+
# confirmed; a failure is reported through `config.on_cache_failure`.
|
|
12
|
+
#
|
|
13
|
+
# @example Dry run (inspect without removing)
|
|
14
|
+
# result = ExpiredAssignmentCleaner.call(dry_run: true)
|
|
15
|
+
# # => { removed: 0, invalidated: 0, expired_count: 5, affected_user_ids: [1, 2, 3] }
|
|
16
|
+
#
|
|
17
|
+
# Each assignment is re-read, locked, when it is destroyed: one that was
|
|
18
|
+
# renewed (or removed) since the run selected it is left alone, and its
|
|
19
|
+
# `assignment.revoked` snapshot is the row as it was at that moment.
|
|
20
|
+
#
|
|
21
|
+
# The audit log: assignments are destroyed through their callbacks (one
|
|
22
|
+
# `assignment.revoked` each), then ONE `cleanup.expired_assignments` event
|
|
23
|
+
# names the sets and the counts — only when something was removed, so a
|
|
24
|
+
# nightly run over nothing leaves no noise. Its data holds the number of
|
|
25
|
+
# affected users and at most AFFECTED_USER_IDS_LIMIT of their ids
|
|
26
|
+
# (`affected_user_ids_truncated` says when there were more; each
|
|
27
|
+
# `assignment.revoked` names its own user). `actor:` (a user, an
|
|
28
|
+
# Audit::Actor or a label String) and `reason:` say who ran it and why;
|
|
29
|
+
# without a reason (none given, none in the context) the events carry
|
|
30
|
+
# DEFAULT_REASON. A dry run writes nothing.
|
|
31
|
+
#
|
|
32
|
+
# Tenants: with a tenant ambient, that tenant's assignments. With none
|
|
33
|
+
# (the rake task) every tenant's — also under acts_as_tenant's
|
|
34
|
+
# require_tenant — and the summary is written in each tenant that lost
|
|
35
|
+
# assignments (its counts) plus once without a tenant (the totals), each
|
|
36
|
+
# with `across_tenants: true` (Concerns::AcrossTenants).
|
|
37
|
+
#
|
|
38
|
+
# @api public
|
|
39
|
+
class ExpiredAssignmentCleaner
|
|
40
|
+
include Concerns::AuditedOperation
|
|
41
|
+
include Concerns::AcrossTenants
|
|
42
|
+
|
|
43
|
+
DEFAULT_REASON = 'expired'.freeze
|
|
44
|
+
|
|
45
|
+
# The most user ids the summary event stores: the event's data must
|
|
46
|
+
# stay well inside a MySQL TEXT column (64 KB) whatever the run's size.
|
|
47
|
+
AFFECTED_USER_IDS_LIMIT = 100
|
|
48
|
+
|
|
49
|
+
# Removes expired assignments and invalidates the affected users' cached permissions.
|
|
50
|
+
# @return [Hash] see {#call}
|
|
51
|
+
def self.call(...) = new(...).call
|
|
52
|
+
|
|
53
|
+
def initialize(dry_run: false, actor: nil, reason: nil)
|
|
54
|
+
@dry_run = dry_run
|
|
55
|
+
@actor = actor
|
|
56
|
+
@reason = reason
|
|
57
|
+
@removed_count = 0
|
|
58
|
+
@invalidated_count = 0
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @return [Hash] `{ removed:, invalidated: }` (plus `expired_count:` and `affected_user_ids:` for a dry run)
|
|
62
|
+
def call
|
|
63
|
+
across_tenants do
|
|
64
|
+
expired_records = PermissionForge::PermissionSetUser.expired
|
|
65
|
+
next dry_run_result(expired_records.count, expired_records.distinct.pluck(:user_id)) if @dry_run
|
|
66
|
+
|
|
67
|
+
audited(actor: @actor, reason: reason) { clean(expired_records.order(:id).pluck(:id)) }
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def reason
|
|
74
|
+
return @reason if @reason.present? || PermissionForge::Audit::Context[:reason].present?
|
|
75
|
+
|
|
76
|
+
DEFAULT_REASON
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def clean(candidate_ids)
|
|
80
|
+
removed = candidate_ids.filter_map { |id| destroy_if_still_expired(id) }
|
|
81
|
+
@removed_count = removed.size
|
|
82
|
+
|
|
83
|
+
confirmed = invalidate_users(removed.map(&:user_id).uniq)
|
|
84
|
+
@invalidated_count = confirmed.size
|
|
85
|
+
record_summary(removed, confirmed) if @removed_count.positive?
|
|
86
|
+
|
|
87
|
+
{ removed: @removed_count, invalidated: @invalidated_count }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# The row as it is now, locked until it is gone; nil when it is no
|
|
91
|
+
# longer expired (renewed) or no longer there.
|
|
92
|
+
def destroy_if_still_expired(id)
|
|
93
|
+
PermissionForge::PermissionSetUser.transaction do
|
|
94
|
+
record = PermissionForge::PermissionSetUser.expired.lock.find_by(id: id)
|
|
95
|
+
record&.destroy
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# One event, or one per tenant plus the totals (record_per_tenant),
|
|
100
|
+
# each with the counts of its own rows.
|
|
101
|
+
def record_summary(records, confirmed_user_ids)
|
|
102
|
+
sets = PermissionForge::PermissionSet.where(id: records.map(&:permission_set_id).uniq).index_by(&:id)
|
|
103
|
+
record_per_tenant('cleanup.expired_assignments', records) do |rows|
|
|
104
|
+
SummaryData.new(rows, sets, confirmed_user_ids).to_h
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# The data of one cleanup.expired_assignments event, for the removed
|
|
109
|
+
# rows it covers.
|
|
110
|
+
#
|
|
111
|
+
# @api private
|
|
112
|
+
SummaryData = Struct.new(:rows, :sets_by_id, :confirmed_user_ids) do
|
|
113
|
+
def to_h
|
|
114
|
+
{ 'operation' => 'cleanup_expired_assignments', 'removed' => rows.size,
|
|
115
|
+
'invalidated' => (user_ids & confirmed_user_ids).size, **bounded_user_ids,
|
|
116
|
+
'set_uuids' => sets.map(&:uuid).uniq, 'set_names' => sets.map(&:name).uniq, 'dry_run' => false }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
private
|
|
120
|
+
|
|
121
|
+
def user_ids
|
|
122
|
+
@user_ids ||= rows.map(&:user_id).uniq
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def sets
|
|
126
|
+
sets_by_id.values_at(*rows.map(&:permission_set_id).uniq.sort).compact
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def bounded_user_ids
|
|
130
|
+
{ 'affected_user_count' => user_ids.size,
|
|
131
|
+
'affected_user_ids' => user_ids.first(AFFECTED_USER_IDS_LIMIT).map(&:to_s),
|
|
132
|
+
'affected_user_ids_truncated' => user_ids.size > AFFECTED_USER_IDS_LIMIT }
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
private_constant :SummaryData
|
|
136
|
+
|
|
137
|
+
def dry_run_result(expired_count, affected_user_ids)
|
|
138
|
+
{ removed: 0, invalidated: 0, expired_count: expired_count, affected_user_ids: affected_user_ids }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# @return [Array] the ids of the users whose invalidation the store
|
|
142
|
+
# confirmed — not what a host's override of the method returns
|
|
143
|
+
def invalidate_users(user_ids)
|
|
144
|
+
confirmed = []
|
|
145
|
+
PermissionForge.configuration.user_model.where(id: user_ids).find_each do |user|
|
|
146
|
+
confirmed << user.id if PermissionForge::Cache::Failure.none? { user.invalidate_permissions }
|
|
147
|
+
end
|
|
148
|
+
confirmed
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
module PermissionForge
|
|
2
|
+
module Services
|
|
3
|
+
# Gives a model's declared extra (virtual) fields their implicit entry when
|
|
4
|
+
# nothing configured them: readable when the resource grants read,
|
|
5
|
+
# editable when it grants edit, never masked.
|
|
6
|
+
#
|
|
7
|
+
# Strictly ADDITIVE: a name that already has an entry (a ResourceField row,
|
|
8
|
+
# or a variant of one) is left untouched, so an extra field can never
|
|
9
|
+
# unmask or grant edit on a field an admin configured. PermissionHashBuilder
|
|
10
|
+
# calls this once per resource, after every granting set is merged, so a
|
|
11
|
+
# row in ANY of the user's sets wins whatever order the sets are in, and
|
|
12
|
+
# the resource flags it follows are the merged ones (edit from any set).
|
|
13
|
+
#
|
|
14
|
+
# Only names the model declares through extra_fields_method are read, and
|
|
15
|
+
# only under extra_fields_default_access = :grant. Under :unconfigured
|
|
16
|
+
# nothing is added: the field follows unconfigured_field_strategy, like a
|
|
17
|
+
# column. Names the admin merely discovers never get an entry here.
|
|
18
|
+
#
|
|
19
|
+
# An error raised by the host's extra_fields_method is not rescued: a
|
|
20
|
+
# broken method must fail the build of the hash, not quietly change it.
|
|
21
|
+
#
|
|
22
|
+
# @api private
|
|
23
|
+
class ExtraFieldDefaults
|
|
24
|
+
# The entry's mask part: an implicit entry is never masked.
|
|
25
|
+
UNMASKED = { mask_access: false, mask_strategy: 'full' }.freeze
|
|
26
|
+
|
|
27
|
+
# @param entry [Hash] the resource's entry in the hash being built:
|
|
28
|
+
# its :fields (written in place) and its merged :permissions
|
|
29
|
+
# @return [Hash] the fields, with implicit entries added
|
|
30
|
+
def self.call(entry:, resource_name:, configuration: PermissionForge.configuration)
|
|
31
|
+
new(entry:, resource_name:, configuration:).call
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# The implicit entry for a resource with the given merged permissions.
|
|
35
|
+
# Only an exact `true` grants: the entry used to be read + edit for
|
|
36
|
+
# everyone who could read the resource, so a read-only user could write
|
|
37
|
+
# a declared virtual field (before 0.14).
|
|
38
|
+
#
|
|
39
|
+
# @param permissions [Hash] the resource's merged :permissions
|
|
40
|
+
# @return [Hash] a new entry
|
|
41
|
+
def self.implicit_entry(permissions)
|
|
42
|
+
{ read_access: permissions[:read_access] == true, edit_access: permissions[:edit_access] == true, **UNMASKED }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def initialize(entry:, resource_name:, configuration:)
|
|
46
|
+
@fields = entry.fetch(:fields)
|
|
47
|
+
@permissions = entry.fetch(:permissions)
|
|
48
|
+
@resource_name = resource_name.to_s
|
|
49
|
+
@configuration = configuration
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @return [Hash] the fields hash, with implicit entries added
|
|
53
|
+
def call
|
|
54
|
+
declared_names.each { |name| @fields[name] ||= self.class.implicit_entry(@permissions) }
|
|
55
|
+
@fields
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
# @fields is String-keyed until the cache symbolizes it. A Symbol name
|
|
61
|
+
# would miss the collision check, sit beside the configured String key,
|
|
62
|
+
# and replace it once both become Symbols — so VirtualFields.extra_names
|
|
63
|
+
# returns Strings.
|
|
64
|
+
def declared_names
|
|
65
|
+
return [] unless @configuration.grant_extra_fields_by_default?
|
|
66
|
+
|
|
67
|
+
PermissionForge::VirtualFields.extra_names(@resource_name.safe_constantize, @configuration)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
module PermissionForge
|
|
2
|
+
module Services
|
|
3
|
+
# Merges the active ResourceField rows of one resource into a fields hash.
|
|
4
|
+
# Extracted from PermissionHashBuilder for SRP, which calls it once per
|
|
5
|
+
# granting set.
|
|
6
|
+
#
|
|
7
|
+
# Rows only: the implicit entries for a model's extra (virtual) fields are
|
|
8
|
+
# added by ExtraFieldDefaults, once per resource, after every set is merged.
|
|
9
|
+
#
|
|
10
|
+
# {.merge_row} is the one definition of merging a row into an entry; the
|
|
11
|
+
# admin's Admin::EffectiveFields explains fields with it.
|
|
12
|
+
#
|
|
13
|
+
# @api private
|
|
14
|
+
class FieldPermissionMerger
|
|
15
|
+
FIELD_PERMISSION_OPTIONS = %i[read_access edit_access].freeze
|
|
16
|
+
FIELD_MASK_OPTIONS = %i[mask_access mask_strategy].freeze
|
|
17
|
+
# The mask of an entry no row that grants read has decided yet.
|
|
18
|
+
DEFAULT_MASK_STRATEGY = 'full'.freeze
|
|
19
|
+
|
|
20
|
+
def self.call(...) = new(...).call
|
|
21
|
+
|
|
22
|
+
# Merges one row into one entry, across granting sets:
|
|
23
|
+
# - read and edit are OR-ed (any set that grants wins);
|
|
24
|
+
# - the mask is decided by the rows that GRANT READ alone: AND-ed (any
|
|
25
|
+
# such set that unmasks wins), with the first such row's strategy.
|
|
26
|
+
# A row that grants no read (the admin UI's Hidden: read=f, mask=f)
|
|
27
|
+
# never votes — it used to take part in the AND, so Masked in one set
|
|
28
|
+
# plus Hidden in another read as plaintext, in either order.
|
|
29
|
+
# The four keys are always present (the hash format is a contract).
|
|
30
|
+
#
|
|
31
|
+
# @param entry [Hash] the field's entry so far, written in place
|
|
32
|
+
# @param field [ResourceField, #read_access] the row
|
|
33
|
+
# @return [Hash] the entry
|
|
34
|
+
def self.merge_row(entry, field)
|
|
35
|
+
# A row that granted read has merged the mask already: read_access is
|
|
36
|
+
# OR-ed, so it is true from that row on and never before.
|
|
37
|
+
decided = entry[:read_access] == true
|
|
38
|
+
FIELD_PERMISSION_OPTIONS.each { |action| entry[action] ||= field.public_send(action) }
|
|
39
|
+
merge_mask(entry, field, decided)
|
|
40
|
+
entry
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.merge_mask(entry, field, decided)
|
|
44
|
+
unless field.read_access
|
|
45
|
+
entry[:mask_access] = false unless entry.key?(:mask_access)
|
|
46
|
+
entry[:mask_strategy] ||= DEFAULT_MASK_STRATEGY
|
|
47
|
+
return
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
entry[:mask_access] = decided ? entry[:mask_access] && field.mask_access : field.mask_access
|
|
51
|
+
entry[:mask_strategy] = field.mask_strategy unless decided
|
|
52
|
+
end
|
|
53
|
+
private_class_method :merge_mask
|
|
54
|
+
|
|
55
|
+
def initialize(fields:, resource:, configuration: PermissionForge.configuration)
|
|
56
|
+
@fields = fields
|
|
57
|
+
@resource = resource
|
|
58
|
+
@configuration = configuration
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def call
|
|
62
|
+
@resource.resource_fields.select(&:is_active).each do |field|
|
|
63
|
+
next unless field.is_active
|
|
64
|
+
|
|
65
|
+
compute_field_variants(field.name).each { |variant| set_field_permissions(variant, field) }
|
|
66
|
+
set_field_permissions(field.name, field)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
@fields
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def compute_field_variants(field_name)
|
|
75
|
+
@configuration.field_variants_for(field_name)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def set_field_permissions(field_name, field)
|
|
79
|
+
self.class.merge_row(@fields[field_name] ||= {}, field)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
module PermissionForge
|
|
2
|
+
module Services
|
|
3
|
+
# Filters and masks response attributes based on the current user's field permissions.
|
|
4
|
+
# Fields without read access are removed; fields with mask_access are masked using MaskValue.
|
|
5
|
+
#
|
|
6
|
+
# Fail-closed posture: when a user is present but has NO field permissions for
|
|
7
|
+
# the resource (unmapped resource, cache re-entrancy), every attribute is treated
|
|
8
|
+
# as unconfigured and routed through `unconfigured_field_strategy` (default :block),
|
|
9
|
+
# so an authenticated user never receives raw fields for a resource they lack
|
|
10
|
+
# configured access to. The nil-user case follows `response_filter_nil_user_strategy`.
|
|
11
|
+
#
|
|
12
|
+
# Each call is one read for the field.access audit event
|
|
13
|
+
# (Audit::FieldAccess): the record is named by the resource and the hash's
|
|
14
|
+
# `id`, the hidden, masked and unconfigured keys are listed, and the
|
|
15
|
+
# legacy on_field_filtered hook is called for every configured key, as
|
|
16
|
+
# before.
|
|
17
|
+
#
|
|
18
|
+
# @example
|
|
19
|
+
# filtered = FiltersResponseAttributes.call(
|
|
20
|
+
# attributes: { name: 'John', email: 'john@example.com' },
|
|
21
|
+
# resource_name: 'Customer'
|
|
22
|
+
# )
|
|
23
|
+
#
|
|
24
|
+
# @api public
|
|
25
|
+
class FiltersResponseAttributes
|
|
26
|
+
# Filters a response hash by the user's field-level read access (does not mutate it).
|
|
27
|
+
# @param attributes [Hash]
|
|
28
|
+
# @param resource_name [String] the model name the fields belong to
|
|
29
|
+
# @param user [Object, nil] defaults to the current user
|
|
30
|
+
# @return [Hash]
|
|
31
|
+
def self.call(attributes:, resource_name:, user: nil)
|
|
32
|
+
new(attributes: attributes, resource_name: resource_name, user: user).call
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def initialize(attributes:, resource_name:, user: nil)
|
|
36
|
+
@attributes = attributes
|
|
37
|
+
@resource_name = resource_name.to_s
|
|
38
|
+
@user = user || PermissionForge.current_user
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @return [Hash] a new hash without the fields the user may not read, masked where required
|
|
42
|
+
def call
|
|
43
|
+
PermissionForge::Audit::FieldAccess.read(@attributes, @resource_name, record_id: record_id) do |frame|
|
|
44
|
+
@frame = frame
|
|
45
|
+
next handle_nil_user if @user.blank?
|
|
46
|
+
|
|
47
|
+
# No early return on blank field_permissions: an authenticated user with no
|
|
48
|
+
# configured access to the resource must still be filtered (fail closed),
|
|
49
|
+
# not handed the raw attributes.
|
|
50
|
+
filter_and_mask
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
UNCONFIGURED_FIELD_STRATEGIES = {
|
|
55
|
+
pass_through: ->(_resource_name, _key, value) { [true, value] },
|
|
56
|
+
warn: lambda { |resource_name, key, value|
|
|
57
|
+
Rails.logger.warn("PermissionForge: unconfigured field '#{key}' on #{resource_name} passed through unmasked")
|
|
58
|
+
[true, value]
|
|
59
|
+
},
|
|
60
|
+
mask_all: ->(_resource_name, _key, value) { [true, MaskValue.call(value, strategy: 'full')] },
|
|
61
|
+
block: ->(_resource_name, _key, _value) { [false, nil] }
|
|
62
|
+
}.freeze
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
# When there is no current user we cannot evaluate permissions. The behavior
|
|
67
|
+
# is explicit and configurable via `response_filter_nil_user_strategy`:
|
|
68
|
+
# :bypass — return raw attributes (system/console context; default, backward-compatible)
|
|
69
|
+
# :block — return {} (strict fail-closed)
|
|
70
|
+
def handle_nil_user
|
|
71
|
+
if PermissionForge.configuration.response_filter_nil_user_strategy == :block
|
|
72
|
+
note_all(:hidden)
|
|
73
|
+
{}
|
|
74
|
+
else
|
|
75
|
+
note_all(:bypassed)
|
|
76
|
+
@attributes
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Without a user every key is decided the same way (the hook was never
|
|
81
|
+
# called for these).
|
|
82
|
+
def note_all(action)
|
|
83
|
+
@attributes.each_key { |key| @frame.add(@resource_name, key, action, nil, hook: false) } if @frame
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# The id in the hash, when it has one (the hash's own key, not a reader).
|
|
87
|
+
def record_id
|
|
88
|
+
return unless @attributes.respond_to?(:key?)
|
|
89
|
+
|
|
90
|
+
@attributes.key?(:id) ? @attributes[:id] : @attributes['id']
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def filter_and_mask
|
|
94
|
+
@result = {}
|
|
95
|
+
@attributes.each { |key, value| process_field(key, value) }
|
|
96
|
+
@result
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def process_field(key, value)
|
|
100
|
+
field = field_permissions[key.to_sym]
|
|
101
|
+
|
|
102
|
+
if field.nil?
|
|
103
|
+
include_field, masked_value = handle_unconfigured_field(key, value)
|
|
104
|
+
@result[key] = masked_value if include_field
|
|
105
|
+
@frame&.add(@resource_name, key, :unconfigured, @user, hook: false)
|
|
106
|
+
else
|
|
107
|
+
apply_configured_field(key, value, field)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def apply_configured_field(key, value, field)
|
|
112
|
+
unless field[:read_access]
|
|
113
|
+
notify_field_filtered(key, :hidden)
|
|
114
|
+
return
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
if field[:mask_access]
|
|
118
|
+
@result[key] = MaskValue.call(value, strategy: field[:mask_strategy] || 'full')
|
|
119
|
+
notify_field_filtered(key, :masked)
|
|
120
|
+
else
|
|
121
|
+
@result[key] = value
|
|
122
|
+
notify_field_filtered(key, :passed)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def notify_field_filtered(field_name, action)
|
|
127
|
+
@frame&.add(@resource_name, field_name, action, @user)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def handle_unconfigured_field(key, value)
|
|
131
|
+
strategy_key = PermissionForge.configuration.unconfigured_field_strategy
|
|
132
|
+
# Unknown strategy keys fall back to :block (fail closed), never pass-through.
|
|
133
|
+
strategy = UNCONFIGURED_FIELD_STRATEGIES.fetch(strategy_key, UNCONFIGURED_FIELD_STRATEGIES[:block])
|
|
134
|
+
strategy.call(@resource_name, key, value)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# The user's field entries for the resource: those of the model the
|
|
138
|
+
# name names and of its STI ancestors (ResourceChain); {} without any.
|
|
139
|
+
def field_permissions
|
|
140
|
+
@field_permissions ||= PermissionForge::ResourceChain.fields(@user, PermissionForge::ResourceChain.names_for(@resource_name))
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Services
|
|
5
|
+
class GdprFieldAuditor
|
|
6
|
+
# Plain-text rendering of a GdprFieldAuditor report for terminals and
|
|
7
|
+
# logs. One line per finding, whatever its type, so the count in the
|
|
8
|
+
# header is the number of lines below it.
|
|
9
|
+
#
|
|
10
|
+
# @api private
|
|
11
|
+
class TextReport
|
|
12
|
+
CLEAN = 'All GDPR-sensitive fields are properly masked.'
|
|
13
|
+
|
|
14
|
+
LINES = {
|
|
15
|
+
unmasked_field: lambda { |finding|
|
|
16
|
+
"[UNMASKED] #{finding[:resource_name]}.#{finding[:field_name]} " \
|
|
17
|
+
"— matches pattern '#{finding[:suggested_strategy]}'"
|
|
18
|
+
},
|
|
19
|
+
unmapped_column: lambda { |finding|
|
|
20
|
+
"[UNMAPPED] #{finding[:resource]}.#{finding[:column]} " \
|
|
21
|
+
"— matches pattern '#{finding[:pattern_matched]}', no active ResourceField"
|
|
22
|
+
},
|
|
23
|
+
implicit_virtual_grant: lambda { |finding|
|
|
24
|
+
"[VIRTUAL] #{finding[:permission_set]} › #{finding[:resource_name]}.#{finding[:field_name]} " \
|
|
25
|
+
"— matches a GDPR pattern (suggested mask: '#{finding[:suggested_strategy]}'), " \
|
|
26
|
+
'no active ResourceField, readable and editable by default'
|
|
27
|
+
},
|
|
28
|
+
view_all_on_pii_resource: lambda { |finding|
|
|
29
|
+
"[VIEW ALL] #{finding[:permission_set]} › #{finding[:resource_name]} " \
|
|
30
|
+
"— #{finding[:bypasses].join(' + ')} skips row-level rules; GDPR-sensitive fields " \
|
|
31
|
+
"on this resource: #{finding[:pii_fields].join(', ')}"
|
|
32
|
+
},
|
|
33
|
+
cross_set_unmasking: lambda { |finding|
|
|
34
|
+
"[OVERRIDES MASK] #{finding[:permission_set]} › #{finding[:resource_name]}.#{finding[:field_name]} " \
|
|
35
|
+
'— unmasked here, masked in another Permission Set; a person in both sees the full value'
|
|
36
|
+
}
|
|
37
|
+
}.freeze
|
|
38
|
+
|
|
39
|
+
# A finding type added to the auditor without a line here still prints.
|
|
40
|
+
FALLBACK = lambda { |finding|
|
|
41
|
+
"[#{finding[:type].to_s.upcase.tr('_', ' ')}] " \
|
|
42
|
+
"#{finding.except(:type).map { |key, value| "#{key}: #{value}" }.join(', ')}"
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
def initialize(report)
|
|
46
|
+
@report = report
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def to_s
|
|
50
|
+
return CLEAN if @report.empty?
|
|
51
|
+
|
|
52
|
+
["GDPR audit found #{@report.size} issue(s):", *@report.map { |finding| " #{line(finding)}" }].join("\n")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def line(finding)
|
|
58
|
+
LINES.fetch(finding[:type], FALLBACK).call(finding)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|