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,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
# Resolves pasted emails/names (one per line or comma-separated) to people
|
|
6
|
+
# for bulk assignment. Exact, case-insensitive matches only — a near miss
|
|
7
|
+
# is reported as "not found" rather than guessed. Only people within
|
|
8
|
+
# config.user_list_resolver match (PeopleDirectory#find_all): anyone else
|
|
9
|
+
# is "not found" too.
|
|
10
|
+
class BulkPeopleMatcher
|
|
11
|
+
FIRST_LAST_COLUMNS = [%w[first_name last_name], %w[firstname lastname]].freeze
|
|
12
|
+
# Same-named people considered per pasted line before the resolver
|
|
13
|
+
# narrows them.
|
|
14
|
+
CANDIDATES_PER_TOKEN = 20
|
|
15
|
+
|
|
16
|
+
Result = Struct.new(:to_assign, :already, :not_found, keyword_init: true)
|
|
17
|
+
|
|
18
|
+
# @param directory [PeopleDirectory, nil] share the request's directory,
|
|
19
|
+
# so an array-returning resolver is evaluated once per request
|
|
20
|
+
def initialize(configuration: PermissionForge.configuration, directory: nil)
|
|
21
|
+
@configuration = configuration
|
|
22
|
+
@directory = directory
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @param text [String] pasted identifiers
|
|
26
|
+
# @param user_ids [Array] people picked with checkboxes
|
|
27
|
+
# @param permission_set [PermissionSet]
|
|
28
|
+
def call(text:, user_ids:, permission_set:)
|
|
29
|
+
found, not_found = resolve(tokens(text))
|
|
30
|
+
picked(user_ids).each { |user| found[user.id] = user }
|
|
31
|
+
|
|
32
|
+
already, to_assign = partition_assigned(found.values, permission_set)
|
|
33
|
+
Result.new(to_assign: to_assign, already: already, not_found: not_found)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
# [already assigned, not yet assigned]
|
|
39
|
+
def partition_assigned(users, permission_set)
|
|
40
|
+
assigned = permission_set.permission_set_users.where(user_id: users.map(&:id)).pluck(:user_id)
|
|
41
|
+
users.partition { |user| assigned.include?(user.id) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def tokens(text)
|
|
45
|
+
PeopleDirectory.search_text(text).split(/[\n,;]+/).map(&:strip).reject(&:empty?).uniq
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# [{ id => user } for matched tokens, unmatched tokens]
|
|
49
|
+
def resolve(tokens)
|
|
50
|
+
found = {}
|
|
51
|
+
not_found = tokens.reject do |token|
|
|
52
|
+
user = lookup(token)
|
|
53
|
+
found[user.id] = user if user
|
|
54
|
+
end
|
|
55
|
+
[found, not_found]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def model
|
|
59
|
+
@configuration.user_model
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def lookup(token)
|
|
63
|
+
needle = token.downcase
|
|
64
|
+
return exact(column_if_present('email'), needle) if token.include?('@')
|
|
65
|
+
|
|
66
|
+
exact(name_column, needle) || first_last_match(needle)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def exact(column, needle)
|
|
70
|
+
column && first_listed(model.where(model.arel_table[column].lower.eq(needle)))
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# The first match that config.user_list holds.
|
|
74
|
+
def first_listed(matches)
|
|
75
|
+
directory.find_all(matches.limit(CANDIDATES_PER_TOKEN).ids).first
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def directory
|
|
79
|
+
@directory ||= PeopleDirectory.new(configuration: @configuration)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def column_if_present(column)
|
|
83
|
+
column if model.column_names.include?(column)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# "Ana Krasniqi" against separate first/last name columns.
|
|
87
|
+
def first_last_match(needle)
|
|
88
|
+
first, last = needle.split(/\s+/, 2)
|
|
89
|
+
pair = first_last_columns
|
|
90
|
+
return nil unless pair && last
|
|
91
|
+
|
|
92
|
+
table = model.arel_table
|
|
93
|
+
first_listed(model.where(table[pair.first].lower.eq(first).and(table[pair.last].lower.eq(last))))
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def first_last_columns
|
|
97
|
+
FIRST_LAST_COLUMNS.find { |cols| (cols - model.column_names).empty? }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def name_column
|
|
101
|
+
(%w[name full_name display_name] & model.column_names).first
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def picked(ids)
|
|
105
|
+
directory.find_all(ids)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
# Batched edits to one Permission Set (set header, resource CRUD/scope
|
|
6
|
+
# flags, field levels, new fields, action toggles, new actions) as
|
|
7
|
+
# submitted by the editor forms.
|
|
8
|
+
#
|
|
9
|
+
# Computes the list of real changes — after the same cascades the UI
|
|
10
|
+
# shows (Read off => Edit/Delete/View All/Modify All off and Editable
|
|
11
|
+
# fields downgraded; Edit off => Modify All off and Editable fields
|
|
12
|
+
# downgraded) — and applies them in one transaction. Muting sets have
|
|
13
|
+
# inverted semantics, so no cascades or dependencies apply to them.
|
|
14
|
+
#
|
|
15
|
+
# Params shape (all optional):
|
|
16
|
+
# set: { name:, description:, is_active:, permission_set_group_id: }
|
|
17
|
+
# resources: { "<id>" => { read_access: "1", ..., label:, description: } }
|
|
18
|
+
# fields: { "<field id>" => { level: "masked", mask_strategy:, label:, description:, is_active: } }
|
|
19
|
+
# new_fields: { "<resource id>" => { "<column>" => { level: } } }
|
|
20
|
+
# actions: { "<action id>" => { is_permitted: "1" } }
|
|
21
|
+
# bulk: { resource_id:, keys: [...], level: }
|
|
22
|
+
# new_actions: { "<resource id>" => { "<key>" => { is_permitted: "1" } } }
|
|
23
|
+
# bulk_actions: { resource_id:, is_permitted: "1" } — every discovered action without a row
|
|
24
|
+
class ChangeSet
|
|
25
|
+
# Collaborators, in evaluation order: later parts see earlier parts'
|
|
26
|
+
# in-memory edits (field levels depend on the resource's new Edit flag).
|
|
27
|
+
PARTS = [
|
|
28
|
+
ChangeSetParts::SetAttrs,
|
|
29
|
+
ChangeSetParts::ResourceFlags,
|
|
30
|
+
ChangeSetParts::FieldLevels,
|
|
31
|
+
ChangeSetParts::NewFields,
|
|
32
|
+
ChangeSetParts::Actions,
|
|
33
|
+
ChangeSetParts::NewActions
|
|
34
|
+
].freeze
|
|
35
|
+
|
|
36
|
+
# kind: :set_attr | :resource_flag | :resource_text | :field_level | :field_attr | :new_field | :action |
|
|
37
|
+
# :new_action
|
|
38
|
+
Change = Struct.new(:kind, :record, :attribute, :from, :to, :cascaded, :resource, keyword_init: true) do
|
|
39
|
+
# Blank-ish values (nil, "") are equal; false is a real value.
|
|
40
|
+
def unchanged?
|
|
41
|
+
from == to || (from.blank? && to.blank? && from != false && to != false)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Captures the record's current value as `from` and assigns `to`.
|
|
45
|
+
# Returns self, or nil (nothing assigned) when effectively unchanged.
|
|
46
|
+
def assign
|
|
47
|
+
self.from = record.public_send(attribute)
|
|
48
|
+
return if unchanged?
|
|
49
|
+
|
|
50
|
+
record.public_send("#{attribute}=", to)
|
|
51
|
+
self
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
class Invalid < StandardError
|
|
56
|
+
attr_reader :record
|
|
57
|
+
|
|
58
|
+
def initialize(record)
|
|
59
|
+
@record = record
|
|
60
|
+
super(record.errors.full_messages.to_sentence)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
attr_reader :permission_set
|
|
65
|
+
|
|
66
|
+
# @param catalog [ControllerCatalog, nil] the app's controller actions,
|
|
67
|
+
# for new_actions; built on first use when nil
|
|
68
|
+
def initialize(permission_set:, params:, sensitive: SensitiveColumns.new, catalog: nil)
|
|
69
|
+
@permission_set = permission_set
|
|
70
|
+
@context = ChangeSetParts::Context.new(permission_set: permission_set, params: normalize(params),
|
|
71
|
+
sensitive: sensitive, catalog: catalog)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def changes
|
|
75
|
+
@changes ||= PARTS.flat_map { |part| part.new(@context).call }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def empty?
|
|
79
|
+
changes.empty?
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Saves every pending record, all or nothing, under one audit
|
|
83
|
+
# correlation id. Its own savepoint (requires_new) rolls back the saves
|
|
84
|
+
# before a failing one — and their audit events — also when the caller's
|
|
85
|
+
# transaction rescues Invalid and goes on to commit.
|
|
86
|
+
#
|
|
87
|
+
# @return [Array<Change>]
|
|
88
|
+
# @raise [Invalid] a record failed validation; nothing was saved
|
|
89
|
+
def apply!
|
|
90
|
+
PermissionForge::Audit.correlate do
|
|
91
|
+
ActiveRecord::Base.transaction(requires_new: true) do
|
|
92
|
+
pending_records.each { |record| save!(record) }
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
changes
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
# Anything but a Hash of sections (a String, an Array) changes nothing.
|
|
101
|
+
def normalize(params)
|
|
102
|
+
hash = if params.respond_to?(:to_unsafe_h) then params.to_unsafe_h
|
|
103
|
+
elsif params.is_a?(Hash) then params
|
|
104
|
+
else {}
|
|
105
|
+
end
|
|
106
|
+
hash.deep_stringify_keys
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def save!(record)
|
|
110
|
+
raise Invalid, record unless record.save
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Records with assigned-but-unsaved attributes, in dependency order.
|
|
114
|
+
def pending_records
|
|
115
|
+
changes.map(&:record).uniq.select { |record| pending?(record) }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def pending?(record)
|
|
119
|
+
record.new_record? || record.changed?
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
module ChangeSetParts
|
|
6
|
+
# Custom action is_permitted toggles (actions of locked resources are ignored).
|
|
7
|
+
class Actions < Base
|
|
8
|
+
def call
|
|
9
|
+
section('actions').filter_map do |id, attrs|
|
|
10
|
+
action = action_index[id.to_s]
|
|
11
|
+
next unless action && attrs.is_a?(Hash) && attrs.key?('is_permitted')
|
|
12
|
+
|
|
13
|
+
assign_change(kind: :action, record: action, attribute: 'is_permitted', to: truthy?(attrs['is_permitted']),
|
|
14
|
+
resource: resource_of(action))
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def action_index
|
|
21
|
+
@action_index ||= resources.values.reject { |r| locked?(r) }.flat_map(&:resource_actions)
|
|
22
|
+
.index_by { |action| action.id.to_s }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
module ChangeSetParts
|
|
6
|
+
# Base for the ChangeSet collaborators. Each part turns one params
|
|
7
|
+
# section into a list of ChangeSet::Change and assigns (never saves) the
|
|
8
|
+
# new values on the in-memory records; ChangeSet#apply! persists them.
|
|
9
|
+
class Base
|
|
10
|
+
def initialize(context)
|
|
11
|
+
@context = context
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
delegate :permission_set, :params, :sensitive, :muting?, :section, :resources, :resource_of,
|
|
17
|
+
to: :@context
|
|
18
|
+
|
|
19
|
+
def change(**attrs)
|
|
20
|
+
ChangeSet::Change.new(**attrs)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Assigns the change on its record; nil when effectively unchanged.
|
|
24
|
+
def assign_change(**attrs)
|
|
25
|
+
change(**attrs).assign
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def valid_level?(level)
|
|
29
|
+
FieldLevel.valid?(level, muting: muting?)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def flags_for(level)
|
|
33
|
+
FieldLevel.flags_for(level, muting: muting?)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The resource for `id`, unless missing or locked.
|
|
37
|
+
def unlocked_resource(id)
|
|
38
|
+
resource = resources[id.to_s]
|
|
39
|
+
resource unless resource.nil? || locked?(resource)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def bulk
|
|
43
|
+
section('bulk')
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Records flagged is_editable: false are managed by the system/API and
|
|
47
|
+
# are never changed from the admin UI (their disabled inputs still post).
|
|
48
|
+
def locked?(record)
|
|
49
|
+
record.respond_to?(:is_editable) && record.is_editable == false
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def truthy?(value)
|
|
53
|
+
Values.truthy?(value)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
module ChangeSetParts
|
|
6
|
+
# State shared by every ChangeSet collaborator: the permission set, the
|
|
7
|
+
# normalized params and the in-memory resources. Each part mutates these
|
|
8
|
+
# same resource/field objects, so later parts see earlier parts' edits
|
|
9
|
+
# (e.g. field levels see the resource's new Edit flag).
|
|
10
|
+
class Context
|
|
11
|
+
attr_reader :permission_set, :params, :sensitive
|
|
12
|
+
|
|
13
|
+
def initialize(permission_set:, params:, sensitive:, catalog: nil)
|
|
14
|
+
@permission_set = permission_set
|
|
15
|
+
@params = params
|
|
16
|
+
@sensitive = sensitive
|
|
17
|
+
@controller_catalog = catalog
|
|
18
|
+
@muting = permission_set.is_muting
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# The app's controller actions (see NewActions). Built on first use:
|
|
22
|
+
# most change sets never read the route table.
|
|
23
|
+
def controller_catalog
|
|
24
|
+
@controller_catalog ||= ControllerCatalog.new
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def muting?
|
|
28
|
+
@muting
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# One params section (e.g. 'fields', 'bulk'), or an empty hash — also
|
|
32
|
+
# when the request sent something else than a Hash there.
|
|
33
|
+
def section(key)
|
|
34
|
+
value = @params[key]
|
|
35
|
+
value.is_a?(Hash) ? value : {}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def resources
|
|
39
|
+
@resources ||= @permission_set.resources.includes(:resource_fields).index_by { |r| r.id.to_s }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# The in-memory resource (with this change set's flag edits applied) — not
|
|
43
|
+
# record.resource, which may be a separately loaded copy.
|
|
44
|
+
def resource_of(record)
|
|
45
|
+
resources[record.resource_id.to_s]
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
module ChangeSetParts
|
|
6
|
+
# Edits to one existing field: level (+ mask strategy), then
|
|
7
|
+
# label/description/active. `resource` is the in-memory resource with
|
|
8
|
+
# this change set's flag edits already applied.
|
|
9
|
+
class FieldEdit
|
|
10
|
+
ATTRS = %w[label description is_active].freeze
|
|
11
|
+
MASK_LEVELS = %w[masked mask].freeze
|
|
12
|
+
CASTS = {
|
|
13
|
+
'label' => ->(value, field) { value.to_s.strip.presence || field.name.humanize },
|
|
14
|
+
'is_active' => ->(value, _field) { Values.truthy?(value) }
|
|
15
|
+
}.freeze
|
|
16
|
+
TEXT_CAST = ->(value, _field) { value.to_s.strip.presence }
|
|
17
|
+
|
|
18
|
+
def initialize(field, resource, muting:)
|
|
19
|
+
@field = field
|
|
20
|
+
@resource = resource
|
|
21
|
+
@muting = muting
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def changes(attrs)
|
|
25
|
+
level_change(attrs) + attr_changes(attrs.slice(*ATTRS))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Cascade: Editable => Read-only. Returns the (cascaded) change or nil.
|
|
29
|
+
def downgrade_to_read
|
|
30
|
+
assign_level('read').first&.tap { |change| change.cascaded = true }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def level_change(attrs)
|
|
36
|
+
level = attrs['level'].to_s
|
|
37
|
+
return [] unless FieldLevel.valid?(level, muting: @muting)
|
|
38
|
+
|
|
39
|
+
assign_level(Values.effective_level(level, @resource, muting: @muting), attrs['mask_strategy'])
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def assign_level(level, strategy = nil)
|
|
43
|
+
from = [FieldLevel.level_of(@field, muting: @muting), @field.mask_strategy]
|
|
44
|
+
write_level(level, strategy)
|
|
45
|
+
return [] if from.first == level && !@field.mask_strategy_changed?
|
|
46
|
+
|
|
47
|
+
[change('level', [level, @field.mask_strategy], kind: :field_level, from: from)]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def write_level(level, strategy)
|
|
51
|
+
@field.assign_attributes(FieldLevel.flags_for(level, muting: @muting))
|
|
52
|
+
@field.mask_strategy = strategy if strategy.present? && MASK_LEVELS.include?(level)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def attr_changes(attrs)
|
|
56
|
+
attrs.filter_map do |attr, value|
|
|
57
|
+
change(attr, CASTS.fetch(attr, TEXT_CAST).call(value, @field), kind: :field_attr).assign
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def change(attribute, to, **attrs)
|
|
62
|
+
ChangeSet::Change.new(record: @field, resource: @resource, attribute: attribute, to: to, **attrs)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
module ChangeSetParts
|
|
6
|
+
# Existing fields: explicit edits and bulk level selections (see
|
|
7
|
+
# FieldEdit), plus the Edit-off cascade to Read-only.
|
|
8
|
+
class FieldLevels < Base
|
|
9
|
+
def call
|
|
10
|
+
requested = requested_field_attrs
|
|
11
|
+
changes = requested.flat_map { |id, attrs| changes_for_field(field_index[id], attrs) }
|
|
12
|
+
changes + cascaded_field_changes(requested.keys)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def field_index
|
|
18
|
+
@field_index ||= resources.values.flat_map(&:resource_fields).index_by { |field| field.id.to_s }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Merges explicit field params with bulk selections.
|
|
22
|
+
def requested_field_attrs
|
|
23
|
+
out = explicit_field_attrs
|
|
24
|
+
level = bulk['level'].to_s
|
|
25
|
+
return out unless valid_level?(level)
|
|
26
|
+
|
|
27
|
+
bulk_fields.each { |field| (out[field.id.to_s] ||= {})['level'] = level }
|
|
28
|
+
out
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def explicit_field_attrs
|
|
32
|
+
section('fields').select { |id, attrs| field_index.key?(id.to_s) && attrs.is_a?(Hash) }.transform_keys(&:to_s)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def bulk_fields
|
|
36
|
+
resource = resources[bulk['resource_id'].to_s]
|
|
37
|
+
return [] unless resource
|
|
38
|
+
|
|
39
|
+
fields = Array(bulk['keys']).filter_map { |key| resource.resource_fields.find { |field| field.name == key.to_s } }
|
|
40
|
+
fields.reject { |field| field.is_editable == false }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def changes_for_field(field, attrs)
|
|
44
|
+
return [] if field.nil? || locked?(field) || locked?(resource_of(field))
|
|
45
|
+
|
|
46
|
+
field_edit(field).changes(attrs)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Editable fields on resources whose Edit (or Read) was just turned off
|
|
50
|
+
# are downgraded to Read-only so field and resource stay consistent.
|
|
51
|
+
def cascaded_field_changes(already)
|
|
52
|
+
return [] if muting?
|
|
53
|
+
|
|
54
|
+
resources.values.reject(&:edit_access).flat_map(&:resource_fields).filter_map do |field|
|
|
55
|
+
field_edit(field).downgrade_to_read if cascadable?(field, already)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# A locked field, or a field of a locked Resource, keeps its level.
|
|
60
|
+
def cascadable?(field, already)
|
|
61
|
+
!already.include?(field.id.to_s) && !locked?(field) && !locked?(resource_of(field)) &&
|
|
62
|
+
FieldLevel.level_of(field, muting: false) == 'edit'
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def field_edit(field)
|
|
66
|
+
FieldEdit.new(field, resource_of(field), muting: muting?)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
module ChangeSetParts
|
|
6
|
+
# New actions: controller actions that discovery lists for a resource and
|
|
7
|
+
# that have no ResourceAction row in this set yet — picked one by one
|
|
8
|
+
# (`new_actions`), or all at once (`bulk_actions`).
|
|
9
|
+
#
|
|
10
|
+
# Only keys the controller catalog lists for the resource are created;
|
|
11
|
+
# free-text keys go through ResourceActionsController. Nothing is created
|
|
12
|
+
# while controller_action_discovery is off, in a locked set, in a locked
|
|
13
|
+
# resource, or for a key that already has a row. A blank is_permitted
|
|
14
|
+
# means "don't add". In a muting set is_permitted means "block".
|
|
15
|
+
class NewActions < Base
|
|
16
|
+
def call
|
|
17
|
+
return [] unless creatable?
|
|
18
|
+
|
|
19
|
+
explicit = hash_section('new_actions').flat_map { |resource_id, keys| explicit_new_actions(resource_id, keys) }
|
|
20
|
+
explicit + bulk_new_actions
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
delegate :controller_catalog, to: :@context
|
|
26
|
+
|
|
27
|
+
def creatable?
|
|
28
|
+
PermissionForge.configuration.controller_action_discovery? && !locked?(permission_set)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# A params section, or {} when the request sent something else there.
|
|
32
|
+
def hash_section(key)
|
|
33
|
+
value = section(key)
|
|
34
|
+
value.is_a?(Hash) ? value : {}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The route table is only read once a row actually asks to be added.
|
|
38
|
+
def explicit_new_actions(resource_id, keys)
|
|
39
|
+
resource = unlocked_resource(resource_id)
|
|
40
|
+
chosen = chosen_keys(keys)
|
|
41
|
+
return [] unless resource && chosen.any?
|
|
42
|
+
|
|
43
|
+
addable = addable_keys(resource)
|
|
44
|
+
chosen.filter_map { |key, permitted| build_new_action(resource, key, permitted) if addable.include?(key) }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# { 'export' => '1' } for the keys with a non-blank choice.
|
|
48
|
+
def chosen_keys(keys)
|
|
49
|
+
return {} unless keys.is_a?(Hash)
|
|
50
|
+
|
|
51
|
+
keys.each_with_object({}) do |(key, attrs), chosen|
|
|
52
|
+
permitted = attrs.is_a?(Hash) ? attrs['is_permitted'].to_s.strip : ''
|
|
53
|
+
chosen[key.to_s] = permitted unless permitted.empty?
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Every discovered key that neither has a row nor an explicit pick.
|
|
58
|
+
def bulk_new_actions
|
|
59
|
+
bulk = hash_section('bulk_actions')
|
|
60
|
+
permitted = bulk['is_permitted'].to_s.strip
|
|
61
|
+
resource = unlocked_resource(bulk['resource_id']) unless permitted.empty?
|
|
62
|
+
return [] unless resource
|
|
63
|
+
|
|
64
|
+
addable_keys(resource).map { |key| build_new_action(resource, key, permitted) }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Discovered keys for the resource without a row — including the rows
|
|
68
|
+
# this change set has already built.
|
|
69
|
+
def addable_keys(resource)
|
|
70
|
+
discovered_keys(resource.name) - existing_names(resource)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# The catalog only lists valid keys; the grep is defence in depth.
|
|
74
|
+
def discovered_keys(resource_name)
|
|
75
|
+
controller_catalog.keys_for(resource_name).grep(PermissionForge::ControllerActions::KEY_FORMAT)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def existing_names(resource)
|
|
79
|
+
resource.resource_actions.map(&:name)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def build_new_action(resource, key, permitted)
|
|
83
|
+
action = resource.resource_actions.build(name: key, label: key.humanize, is_active: true,
|
|
84
|
+
is_permitted: truthy?(permitted))
|
|
85
|
+
change(kind: :new_action, record: action, attribute: 'is_permitted', from: nil, to: action.is_permitted,
|
|
86
|
+
resource: resource)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
module ChangeSetParts
|
|
6
|
+
# New fields: columns without a ResourceField row, picked one by one or
|
|
7
|
+
# through a bulk selection.
|
|
8
|
+
class NewFields < Base
|
|
9
|
+
def call
|
|
10
|
+
explicit = section('new_fields').flat_map { |resource_id, columns| explicit_new_fields(resource_id, columns) }
|
|
11
|
+
explicit + bulk_new_fields
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def explicit_new_fields(resource_id, columns)
|
|
17
|
+
resource = unlocked_resource(resource_id)
|
|
18
|
+
return [] unless resource && columns.is_a?(Hash)
|
|
19
|
+
|
|
20
|
+
existing = existing_names(resource)
|
|
21
|
+
columns.filter_map do |column, attrs|
|
|
22
|
+
name = column.to_s
|
|
23
|
+
next if existing.include?(name) || !attrs.is_a?(Hash)
|
|
24
|
+
|
|
25
|
+
build_new_field(resource, name, attrs['level'].to_s)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def bulk_new_fields
|
|
30
|
+
level = bulk['level'].to_s
|
|
31
|
+
resource = unlocked_resource(bulk['resource_id'])
|
|
32
|
+
return [] unless resource && valid_level?(level)
|
|
33
|
+
|
|
34
|
+
bulk_columns(resource).filter_map { |column| build_new_field(resource, column, level) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Bulk-selected columns that have neither a field nor an explicit pick.
|
|
38
|
+
def bulk_columns(resource)
|
|
39
|
+
Array(bulk['keys']).map(&:to_s).uniq - existing_names(resource) - explicit_columns(resource)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def existing_names(resource)
|
|
43
|
+
resource.resource_fields.map(&:name)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def explicit_columns(resource)
|
|
47
|
+
(params.dig('new_fields', resource.id.to_s) || {}).keys.map(&:to_s)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def build_new_field(resource, column, level)
|
|
51
|
+
return nil unless valid_level?(level) && level != 'leave'
|
|
52
|
+
|
|
53
|
+
level = Values.effective_level(level, resource, muting: muting?)
|
|
54
|
+
field = resource.resource_fields.build(name: column, label: column.humanize, is_active: true, is_editable: true)
|
|
55
|
+
field.assign_attributes(flags_for(level))
|
|
56
|
+
field.mask_strategy = sensitive.match(column)&.strategy || 'full'
|
|
57
|
+
change(kind: :new_field, record: field, attribute: 'level', from: nil, to: [level, field.mask_strategy],
|
|
58
|
+
resource: resource)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|