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,52 @@
|
|
|
1
|
+
module PermissionForge
|
|
2
|
+
module Services
|
|
3
|
+
# Deep-copies a Permission Set Group with all its Permission Sets (each
|
|
4
|
+
# through PermissionSetCloner) — never its members. The group's copy
|
|
5
|
+
# records `permission_set_group.cloned`, each set's copy
|
|
6
|
+
# `permission_set.cloned`; every event of the copy shares one correlation id.
|
|
7
|
+
#
|
|
8
|
+
# @api public
|
|
9
|
+
class PermissionSetGroupCloner
|
|
10
|
+
include Clonable
|
|
11
|
+
|
|
12
|
+
# @param unlock [Boolean] copy the group's sets (and their rows)
|
|
13
|
+
# editable (`is_editable: true`) instead of keeping their locks
|
|
14
|
+
def self.call(permission_set_group:, attrs: {}, unlock: false)
|
|
15
|
+
new(permission_set_group, attrs, unlock: unlock).call
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def initialize(permission_set_group, attrs, unlock: false)
|
|
19
|
+
@group = permission_set_group
|
|
20
|
+
@attrs = attrs
|
|
21
|
+
@unlock = unlock
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @return [PermissionSetGroup] the copy
|
|
25
|
+
def call
|
|
26
|
+
PermissionForge::Audit.correlate do
|
|
27
|
+
@group.class.transaction do
|
|
28
|
+
cloned = duplicate_record(@group, **@attrs)
|
|
29
|
+
clone_permission_sets(cloned)
|
|
30
|
+
record_clone(cloned)
|
|
31
|
+
cloned
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def clone_permission_sets(cloned_group)
|
|
39
|
+
@group.permission_sets.each do |ps|
|
|
40
|
+
attrs = { name: ps.name, permission_set_group: cloned_group }
|
|
41
|
+
attrs[:is_editable] = true if @unlock
|
|
42
|
+
PermissionSetCloner.call(permission_set: ps, attrs: attrs, unlock: @unlock)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def record_clone(cloned)
|
|
47
|
+
PermissionForge::Audit.record('permission_set_group.cloned', record: cloned,
|
|
48
|
+
data: { 'source_uuid' => @group.uuid, 'source_name' => @group.name })
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Services
|
|
5
|
+
class PermitsAttributes
|
|
6
|
+
# Which nested records name a row the current user may reach.
|
|
7
|
+
#
|
|
8
|
+
# A nested record keeps its `id` (without one a payload can only
|
|
9
|
+
# create), and Rails refuses the id of another parent's row — but it
|
|
10
|
+
# loads the rows of the SAME parent through the association, which is
|
|
11
|
+
# not rule-scoped. So a record with an id survives only when the child
|
|
12
|
+
# model has no row-level rules to ask (no PermissionScopedFindable:
|
|
13
|
+
# left to Rails) or the id is inside `permission_scoped` for the current
|
|
14
|
+
# user, checked in one query per payload. Otherwise the whole record
|
|
15
|
+
# goes: dropping the id alone would turn an update into a create. A
|
|
16
|
+
# blank id is a new record, as Rails reads it: nothing to check.
|
|
17
|
+
#
|
|
18
|
+
# @api private
|
|
19
|
+
class NestedRecords
|
|
20
|
+
# One id per record. A plain Hash may carry both 'id' and :id; Rails
|
|
21
|
+
# reads the record with indifferent access (the last key wins), and
|
|
22
|
+
# the guard must not check one id while Rails applies the other. The
|
|
23
|
+
# same id under both keys is kept under one; two ids drop the record.
|
|
24
|
+
#
|
|
25
|
+
# @param record [Hash] a filtered record
|
|
26
|
+
# @return [Hash, nil]
|
|
27
|
+
def self.with_one_id(record)
|
|
28
|
+
return record unless record.key?('id') && record.key?(:id)
|
|
29
|
+
|
|
30
|
+
ids = [record['id'], record[:id]].map { |id| id.to_s.presence }.uniq
|
|
31
|
+
record.except(:id) if ids.size == 1
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @param collection [Boolean] whether the association is a collection
|
|
35
|
+
# (has_many); a has_one / belongs_to payload is one record
|
|
36
|
+
def initialize(child, collection:)
|
|
37
|
+
@child = child
|
|
38
|
+
@collection = collection
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @param payload [Array<Hash>, Hash] the filtered payload: an Array of
|
|
42
|
+
# records, Rails' index Hash, or one record
|
|
43
|
+
# @yield once for every record of a collection that is dropped
|
|
44
|
+
# @return [Array<Hash>, Hash, nil] the payload without the denied
|
|
45
|
+
# records; nil when its one record is denied
|
|
46
|
+
def admit(payload, &)
|
|
47
|
+
return payload unless @child.respond_to?(:permission_scoped)
|
|
48
|
+
return admit_one(payload) unless @collection
|
|
49
|
+
|
|
50
|
+
case payload
|
|
51
|
+
when Array then admit_each(payload, &)
|
|
52
|
+
when Hash then id_key?(payload) ? admit_one(payload) : admit_indexed(payload, &)
|
|
53
|
+
else payload
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def admit_each(records, &)
|
|
60
|
+
denied = denied_ids(records)
|
|
61
|
+
records.select { |record| admitted?(record, denied, &) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def admit_indexed(records, &)
|
|
65
|
+
denied = denied_ids(records.values)
|
|
66
|
+
records.select { |_, record| admitted?(record, denied, &) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def admit_one(record)
|
|
70
|
+
record if denied_ids([record]).empty?
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def admitted?(record, denied)
|
|
74
|
+
return true unless denied.include?(id_of(record))
|
|
75
|
+
|
|
76
|
+
yield if block_given?
|
|
77
|
+
false
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# The ids among the records that permission_scoped does not reach,
|
|
81
|
+
# as Strings, in one query. An id that names no row is denied too.
|
|
82
|
+
def denied_ids(records)
|
|
83
|
+
ids = records.filter_map { |record| id_of(record) }
|
|
84
|
+
return [] if ids.empty?
|
|
85
|
+
|
|
86
|
+
key = @child.primary_key
|
|
87
|
+
ids - @child.permission_scoped.where(key => ids).pluck(key).map(&:to_s)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# As Rails tells one record of a collection from the index form: by
|
|
91
|
+
# the key, whatever it holds.
|
|
92
|
+
def id_key?(record)
|
|
93
|
+
record.key?('id') || record.key?(:id)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# The record's id as a String; nil for none or a blank one.
|
|
97
|
+
def id_of(record)
|
|
98
|
+
return unless record.respond_to?(:key?)
|
|
99
|
+
|
|
100
|
+
id = record.key?('id') ? record['id'] : record[:id]
|
|
101
|
+
id.to_s.presence
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
module PermissionForge
|
|
2
|
+
module Services
|
|
3
|
+
# Filters permitted params by the current user's field-level EDIT permissions.
|
|
4
|
+
#
|
|
5
|
+
# A key is kept when the user may edit that field on the resource, whatever
|
|
6
|
+
# its value: a Hash or an Array is a value like any other (a json column),
|
|
7
|
+
# never a payload to look into. A key the user may not edit is dropped,
|
|
8
|
+
# whatever its value (fail closed).
|
|
9
|
+
#
|
|
10
|
+
# The one exception is a nested payload: `<assoc>_attributes` for an
|
|
11
|
+
# association the model accepts nested attributes for. It is filtered
|
|
12
|
+
# record by record against the ASSOCIATION MODEL's own resource entry — an
|
|
13
|
+
# Array per element, Rails' index Hash per value, a has_one / belongs_to
|
|
14
|
+
# Hash as one record; an element that is not a record is dropped. A
|
|
15
|
+
# record keeps its `id` only when it names a row the user may reach
|
|
16
|
+
# ({NestedRecords}: inside the child model's `permission_scoped`, or the
|
|
17
|
+
# child has no row rules to ask); a denied record is dropped whole. A
|
|
18
|
+
# record keeps its `_destroy` only when the user may delete the model
|
|
19
|
+
# the payload is filtered for (its entry's `delete_access`; Rails would
|
|
20
|
+
# otherwise destroy the row on the strength of an edit grant). When
|
|
21
|
+
# the user's hash has no entry for that model the payload is dropped;
|
|
22
|
+
# config.nested_attributes_parent_fallback = true filters it against the
|
|
23
|
+
# parent's fields and delete access instead (the id check still applies).
|
|
24
|
+
#
|
|
25
|
+
# Non-mutating: the caller's params object is never modified; a new plain Hash
|
|
26
|
+
# is returned.
|
|
27
|
+
#
|
|
28
|
+
# The keys that were dropped are published as params.filtered (when it is
|
|
29
|
+
# stored or heard; nothing is collected otherwise): their paths —
|
|
30
|
+
# `secret`, `supplier_attributes.secret`, `lines_attributes.*.price`,
|
|
31
|
+
# `lines_attributes.*` for an element that is no record — never their
|
|
32
|
+
# values. Without a user every key is dropped (reason nil_user).
|
|
33
|
+
#
|
|
34
|
+
# @example
|
|
35
|
+
# filtered = PermitsAttributes.call(resource: customer, permitted_params: params)
|
|
36
|
+
#
|
|
37
|
+
# @api public
|
|
38
|
+
class PermitsAttributes
|
|
39
|
+
EVENT = 'params.filtered'.freeze
|
|
40
|
+
NESTED_SUFFIX = '_attributes'.freeze
|
|
41
|
+
# The path segment of a record inside a collection payload.
|
|
42
|
+
EACH = '*'.freeze
|
|
43
|
+
# The keys of a nested record that name it rather than set a field.
|
|
44
|
+
RECORD_KEYS = %w[id].freeze
|
|
45
|
+
# The key of a nested record that asks for its destruction.
|
|
46
|
+
DESTROY_KEY = '_destroy'.freeze
|
|
47
|
+
|
|
48
|
+
# Filters incoming params by the user's field-level edit access (does not mutate them).
|
|
49
|
+
# @param resource [String] the model name
|
|
50
|
+
# @param permitted_params [Hash]
|
|
51
|
+
# @return [Hash]
|
|
52
|
+
def self.call(resource:, permitted_params:)
|
|
53
|
+
new(resource: resource, permitted_params: permitted_params).call
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def initialize(resource:, permitted_params:)
|
|
57
|
+
@user = PermissionForge.current_user
|
|
58
|
+
@resource = resource
|
|
59
|
+
@permitted_params = permitted_params
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @return [Hash] a new hash holding only the fields the user may edit
|
|
63
|
+
def call
|
|
64
|
+
if @user.blank?
|
|
65
|
+
report_dropped('nil_user') { each_pair(@permitted_params) { |key, _| dropped(key) } } if nil_user_report?
|
|
66
|
+
return {}
|
|
67
|
+
end
|
|
68
|
+
return {} if resource_class.nil?
|
|
69
|
+
return {} if @permitted_params.blank?
|
|
70
|
+
|
|
71
|
+
report_dropped('field_access') { filter_for(resource_class, @permitted_params) }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
# Runs the filter, collecting the dropped paths in @dropped only when
|
|
77
|
+
# params.filtered is stored or heard, and publishes them.
|
|
78
|
+
def report_dropped(reason)
|
|
79
|
+
return yield unless PermissionForge::Audit.instrument?(EVENT)
|
|
80
|
+
|
|
81
|
+
@dropped = []
|
|
82
|
+
@path = []
|
|
83
|
+
result = yield
|
|
84
|
+
publish_dropped(reason) unless @dropped.empty?
|
|
85
|
+
result
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def nil_user_report?
|
|
89
|
+
!resource_class.nil? && @permitted_params.present? && PermissionForge::Audit.instrument?(EVENT)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def publish_dropped(reason)
|
|
93
|
+
dropped = @dropped.uniq
|
|
94
|
+
record = @resource.is_a?(Class) ? nil : @resource
|
|
95
|
+
PermissionForge::Audit.instrument(EVENT) do
|
|
96
|
+
reference = record ? { record: record } : { record_type: PermissionForge::Audit::Runtime.record_type(resource_class) }
|
|
97
|
+
reference.merge(subject_user_id: PermissionForge::Audit::Runtime.user_id(@user),
|
|
98
|
+
data: { 'model' => resource_class.name, 'reason' => reason, 'dropped' => dropped },
|
|
99
|
+
objects: { user: @user, record: record })
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Records the dropped key's path, when dropped keys are collected. Nil,
|
|
104
|
+
# so that a dropped element leaves nothing in a filter_map.
|
|
105
|
+
def dropped(key)
|
|
106
|
+
@dropped&.push((@path + [key.to_s]).join('.'))
|
|
107
|
+
nil
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def within(segment)
|
|
111
|
+
return yield unless @dropped
|
|
112
|
+
|
|
113
|
+
@path.push(segment.to_s)
|
|
114
|
+
begin
|
|
115
|
+
yield
|
|
116
|
+
ensure
|
|
117
|
+
@path.pop
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def resource_class
|
|
122
|
+
return nil if @resource.nil?
|
|
123
|
+
|
|
124
|
+
@resource.is_a?(Class) ? @resource : @resource.class
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# A NEW hash with the keys editable on `klass`, each nested payload
|
|
128
|
+
# filtered for its own model. Everything else is dropped (fail closed).
|
|
129
|
+
def filter_for(klass, params, record: false)
|
|
130
|
+
fields = editable_fields_for(klass)
|
|
131
|
+
result = {}
|
|
132
|
+
|
|
133
|
+
each_pair(params) do |key, value|
|
|
134
|
+
if (association = nested_association(klass, key))
|
|
135
|
+
filtered = within(key) { filter_nested(klass, association, value) }
|
|
136
|
+
filtered.nil? ? dropped(key) : result[key] = filtered
|
|
137
|
+
elsif fields.key?(key.to_sym) || (record && record_key?(klass, key))
|
|
138
|
+
result[key] = value
|
|
139
|
+
else
|
|
140
|
+
dropped(key)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
result
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# A key of a nested record that names it rather than sets a field —
|
|
148
|
+
# or its `_destroy`, when the user may delete records of `klass`.
|
|
149
|
+
def record_key?(klass, key)
|
|
150
|
+
name = key.to_s
|
|
151
|
+
RECORD_KEYS.include?(name) || (name == DESTROY_KEY && deletable?(klass))
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Whether the user may delete records of `klass`: the delete_access of
|
|
155
|
+
# the entry the class answers to (ResourceChain.grants?). Once per
|
|
156
|
+
# class and call.
|
|
157
|
+
def deletable?(klass)
|
|
158
|
+
@deletable ||= {}
|
|
159
|
+
@deletable.fetch(klass) do
|
|
160
|
+
@deletable[klass] = @user.respond_to?(:permissions_hash) &&
|
|
161
|
+
PermissionForge::ResourceChain.grants?(@user, resource_names(klass), :delete_access)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# The association behind a `<assoc>_attributes` key, when `klass`
|
|
166
|
+
# accepts nested attributes for it. Any other key is a field.
|
|
167
|
+
def nested_association(klass, key)
|
|
168
|
+
name = key.to_s
|
|
169
|
+
return unless name.end_with?(NESTED_SUFFIX) && klass.respond_to?(:nested_attributes_options)
|
|
170
|
+
|
|
171
|
+
assoc_name = name.delete_suffix(NESTED_SUFFIX).to_sym
|
|
172
|
+
klass.reflect_on_association(assoc_name) if klass.nested_attributes_options.key?(assoc_name)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# The payload filtered for the model it is for, minus the records whose
|
|
176
|
+
# id names a row the child's row rules deny (NestedRecords) — or nil,
|
|
177
|
+
# the key dropped as a whole: no entry for that model, not a payload,
|
|
178
|
+
# or its one record denied.
|
|
179
|
+
def filter_nested(parent_klass, association, value)
|
|
180
|
+
child = association_class(association)
|
|
181
|
+
target = nested_target(parent_klass, child)
|
|
182
|
+
return unless target
|
|
183
|
+
|
|
184
|
+
filtered = filter_payload(target, association, value)
|
|
185
|
+
NestedRecords.new(child, collection: association.collection?).admit(filtered) { dropped(EACH) } unless filtered.nil?
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# The model a nested payload is filtered for: the association's own,
|
|
189
|
+
# when the user's hash has an entry for it; the parent, only under
|
|
190
|
+
# config.nested_attributes_parent_fallback (a key editable on the
|
|
191
|
+
# parent then admits the same key on a child the user was never
|
|
192
|
+
# granted); nothing otherwise.
|
|
193
|
+
def nested_target(parent_klass, child)
|
|
194
|
+
return child if child && configured_resource?(child)
|
|
195
|
+
|
|
196
|
+
parent_klass if PermissionForge.configuration.nested_attributes_parent_fallback
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# nil for a polymorphic association (no one model) or one whose model
|
|
200
|
+
# does not load: such a payload is dropped.
|
|
201
|
+
def association_class(association)
|
|
202
|
+
association.klass unless association.polymorphic?
|
|
203
|
+
rescue NameError
|
|
204
|
+
nil
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def filter_payload(target, association, value)
|
|
208
|
+
return filter_record(target, value) unless association.collection?
|
|
209
|
+
return filter_each(target, value) if value.is_a?(Array)
|
|
210
|
+
return unless each_pair?(value)
|
|
211
|
+
|
|
212
|
+
# As Rails reads a collection Hash: one record when it has an id,
|
|
213
|
+
# otherwise the index form, whose values are the records.
|
|
214
|
+
record_with_id?(value) ? within(EACH) { filter_record(target, value) } : filter_indexed(target, value)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# One record (a has_one / belongs_to payload, or one of a collection),
|
|
218
|
+
# with one id (NestedRecords.with_one_id); nil when it is no record or
|
|
219
|
+
# names two ids.
|
|
220
|
+
def filter_record(target, value)
|
|
221
|
+
NestedRecords.with_one_id(filter_for(target, value, record: true)) if each_pair?(value)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def filter_each(target, records)
|
|
225
|
+
records.filter_map do |record|
|
|
226
|
+
filtered = within(EACH) { filter_record(target, record) }
|
|
227
|
+
filtered.nil? ? dropped(EACH) : filtered
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def filter_indexed(target, records)
|
|
232
|
+
result = {}
|
|
233
|
+
each_pair(records) do |index, record|
|
|
234
|
+
filtered = within(EACH) { filter_record(target, record) }
|
|
235
|
+
filtered.nil? ? dropped(EACH) : result[index] = filtered
|
|
236
|
+
end
|
|
237
|
+
result
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def record_with_id?(value)
|
|
241
|
+
value.key?('id') || value.key?(:id)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# The Resource names a model answers to: its own and its STI
|
|
245
|
+
# ancestors' (ResourceChain).
|
|
246
|
+
def resource_names(klass)
|
|
247
|
+
PermissionForge::ResourceChain.names(klass)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def configured_resource?(klass)
|
|
251
|
+
@user.respond_to?(:permissions_hash) && PermissionForge::ResourceChain.entries(@user, resource_names(klass)).any?
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def editable_fields_for(klass)
|
|
255
|
+
return {} unless @user.respond_to?(:permissions_hash)
|
|
256
|
+
|
|
257
|
+
PermissionForge::ResourceChain.fields(@user, resource_names(klass)).select { |_, access| access[:edit_access] }
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Iterates key/value pairs of an ActionController::Parameters or a Hash.
|
|
261
|
+
def each_pair(params, &)
|
|
262
|
+
params.each_pair(&)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def each_pair?(value)
|
|
266
|
+
value.respond_to?(:each_pair)
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
module PermissionForge
|
|
2
|
+
module Services
|
|
3
|
+
# Creates a ResourceField for every column of a resource's model (and declared virtual fields
|
|
4
|
+
# under `extra_fields_default_access = :unconfigured`).
|
|
5
|
+
#
|
|
6
|
+
# @api public
|
|
7
|
+
class ResourceFieldGenerator
|
|
8
|
+
# @param resource [Resource]
|
|
9
|
+
# @param grant_all [Boolean] grant read and edit on the generated fields
|
|
10
|
+
# @return [Array<ResourceField>] the fields created
|
|
11
|
+
def self.call(resource:, grant_all: false)
|
|
12
|
+
new(resource: resource, grant_all: grant_all).call
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(resource:, grant_all: false)
|
|
16
|
+
@resource = resource
|
|
17
|
+
@grant_all = grant_all
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# All or nothing, under one audit correlation id (an enclosing block's,
|
|
21
|
+
# if any): a field that cannot be saved leaves no field and no event.
|
|
22
|
+
def call
|
|
23
|
+
model_class = @resource.name.safe_constantize
|
|
24
|
+
return [] unless model_class
|
|
25
|
+
|
|
26
|
+
fields = PermissionForge::Audit.correlate { PermissionForge::ResourceField.transaction { generate_fields(model_class) } }
|
|
27
|
+
PermissionForge.configuration.record_gdpr_pattern_checksum
|
|
28
|
+
fields
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# A field needs a name: a blank one declared by the model is skipped
|
|
32
|
+
# (and logged) rather than failing "Add resource" / "Sync fields".
|
|
33
|
+
def self.without_blank_names(model_class, names)
|
|
34
|
+
usable = names.reject { |name| name.to_s.strip.empty? }
|
|
35
|
+
skipped = names.size - usable.size
|
|
36
|
+
if skipped.positive?
|
|
37
|
+
Rails.logger&.warn("[PermissionForge] skipped #{skipped} blank field name(s) declared by " \
|
|
38
|
+
"#{model_class.name}.#{PermissionForge.configuration.extra_fields_method}")
|
|
39
|
+
end
|
|
40
|
+
usable
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def generate_fields(model_class)
|
|
46
|
+
existing_names = @resource.resource_fields.pluck(:name)
|
|
47
|
+
columns = model_class.column_names - ignored_fields + virtual_field_names(model_class) - existing_names
|
|
48
|
+
|
|
49
|
+
columns.map do |column_name|
|
|
50
|
+
attrs = build_field_attrs(column_name)
|
|
51
|
+
apply_gdpr_defaults(attrs, column_name) if PermissionForge.configuration.gdpr_defaults
|
|
52
|
+
PermissionForge::ResourceField.create!(attrs)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def build_field_attrs(column_name)
|
|
57
|
+
{
|
|
58
|
+
resource: @resource,
|
|
59
|
+
name: column_name,
|
|
60
|
+
label: column_name.humanize,
|
|
61
|
+
is_active: true,
|
|
62
|
+
is_editable: true,
|
|
63
|
+
read_access: @grant_all,
|
|
64
|
+
edit_access: @grant_all
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def ignored_fields
|
|
69
|
+
PermissionForge.configuration.ignored_fields
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Virtual fields get rows only under extra_fields_default_access =
|
|
73
|
+
# :unconfigured. Under :grant ExtraFieldDefaults already gives declared
|
|
74
|
+
# names read + edit, and a generated row (no access unless grant_all)
|
|
75
|
+
# would take that away from callers that rely on it.
|
|
76
|
+
def virtual_field_names(model_class)
|
|
77
|
+
configuration = PermissionForge.configuration
|
|
78
|
+
return [] if configuration.grant_extra_fields_by_default?
|
|
79
|
+
|
|
80
|
+
self.class.without_blank_names(model_class, PermissionForge::VirtualFields.new(model_class, configuration).names)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def apply_gdpr_defaults(attrs, column_name)
|
|
84
|
+
_pattern, strategy = PermissionForge.configuration.gdpr_field_patterns.find { |regex, _| column_name.match?(regex) }
|
|
85
|
+
return unless strategy
|
|
86
|
+
|
|
87
|
+
attrs[:read_access] = true
|
|
88
|
+
attrs[:mask_access] = true
|
|
89
|
+
attrs[:mask_strategy] = strategy
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module PermissionForge
|
|
2
|
+
module Services
|
|
3
|
+
# Creates a Resource with full CRUD access and its generated fields, all or nothing.
|
|
4
|
+
#
|
|
5
|
+
# @api public
|
|
6
|
+
class ResourceWithFieldsBuilder
|
|
7
|
+
# @param permission_set [PermissionSet]
|
|
8
|
+
# @param resource_name [String] the model name
|
|
9
|
+
# @return [Resource]
|
|
10
|
+
def self.call(permission_set:, resource_name:)
|
|
11
|
+
new(permission_set, resource_name).call
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize(permission_set, resource_name)
|
|
15
|
+
@permission_set = permission_set
|
|
16
|
+
@resource_name = resource_name
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# The resource and its fields are created all or nothing, under one
|
|
20
|
+
# audit correlation id (an enclosing block's, if any).
|
|
21
|
+
def call
|
|
22
|
+
PermissionForge::Audit.correlate do
|
|
23
|
+
PermissionForge::Resource.transaction do
|
|
24
|
+
resource = create_resource
|
|
25
|
+
ResourceFieldGenerator.call(resource: resource, grant_all: true)
|
|
26
|
+
resource
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def create_resource
|
|
34
|
+
PermissionForge::Resource.create!(
|
|
35
|
+
name: @resource_name,
|
|
36
|
+
permission_set: @permission_set,
|
|
37
|
+
read_access: true,
|
|
38
|
+
create_access: true,
|
|
39
|
+
edit_access: true,
|
|
40
|
+
delete_access: true,
|
|
41
|
+
is_active: true,
|
|
42
|
+
is_editable: true
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|