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,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
# Every Permission Set containing a resource, with one field's row there
|
|
6
|
+
# (granting sets first, then by name) and the set's people count. Backs
|
|
7
|
+
# the field drawer's "In other sets" list and "Who can see …?".
|
|
8
|
+
class FieldAcrossSets
|
|
9
|
+
# @return [Array<Hash>] { resource:, set:, field: (ResourceField or nil), people:, in_effect: }
|
|
10
|
+
# in_effect is whether the row (or, without a row, the resource) counts
|
|
11
|
+
# right now, as in ActionAcrossSets#rows.
|
|
12
|
+
def self.call(resource_name, key) = new(resource_name, key).call
|
|
13
|
+
|
|
14
|
+
def initialize(resource_name, key)
|
|
15
|
+
@resource_name = resource_name
|
|
16
|
+
@key = key
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def call
|
|
20
|
+
resources.map do |resource|
|
|
21
|
+
field = field_in(resource)
|
|
22
|
+
{ resource: resource, set: resource.permission_set, field: field, people: people_count(resource),
|
|
23
|
+
in_effect: resource.in_effect? && (field.nil? || field.is_active) }
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def resources
|
|
30
|
+
PermissionForge::Resource.where(name: @resource_name).includes(:permission_set, :resource_fields)
|
|
31
|
+
.sort_by { |resource| sort_key(resource.permission_set) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def sort_key(permission_set)
|
|
35
|
+
[permission_set.is_muting ? 1 : 0, permission_set.name]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def field_in(resource)
|
|
39
|
+
resource.resource_fields.find { |field| field.name == @key }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def people_count(resource)
|
|
43
|
+
people_counts.fetch(resource.permission_set_id, 0)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def people_counts
|
|
47
|
+
@people_counts ||= PermissionForge::PermissionSetUser.active_assignments.group(:permission_set_id).count
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
# What the field drawer (resource_fields/show) shows for one column of a
|
|
6
|
+
# Resource, decided here so the template only lays it out: the level and
|
|
7
|
+
# mask strategy in force, which notes apply, whether the recommendation
|
|
8
|
+
# can be applied, and which person is being explained.
|
|
9
|
+
#
|
|
10
|
+
# Words stay in the template (I18n); this answers which note, not what it
|
|
11
|
+
# says.
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# detail = FieldDetail.new(resource: resource, row: rows.row('email'), rows: rows)
|
|
15
|
+
# detail.locked? # => false
|
|
16
|
+
# detail.unconfigured_note # => :not_configured
|
|
17
|
+
# detail.recommendation # => :apply
|
|
18
|
+
class FieldDetail
|
|
19
|
+
DEFAULT_STRATEGY = 'full'
|
|
20
|
+
CONTEXT_KEYS = %i[rules across people_count explain_people explain explain_user_id].freeze
|
|
21
|
+
|
|
22
|
+
attr_reader :resource, :row, :rows, :rules, :across, :people_count, :explain, :explain_people
|
|
23
|
+
|
|
24
|
+
# rows: the FieldRows the row came from (it knows the model).
|
|
25
|
+
# explain: { user:, field: } for the person picked in "Explain for a person", else nil.
|
|
26
|
+
def initialize(resource:, row:, rows:, configuration: PermissionForge.configuration, **context)
|
|
27
|
+
@resource = resource
|
|
28
|
+
@row = row
|
|
29
|
+
@rows = rows
|
|
30
|
+
@configuration = configuration
|
|
31
|
+
unknown = context.keys - CONTEXT_KEYS
|
|
32
|
+
raise ArgumentError, "unknown context: #{unknown.join(', ')}" if unknown.any?
|
|
33
|
+
|
|
34
|
+
@rules = context.fetch(:rules, [])
|
|
35
|
+
@across = context[:across]
|
|
36
|
+
@people_count = context[:people_count]
|
|
37
|
+
@explain_people = context.fetch(:explain_people, [])
|
|
38
|
+
@explain = context[:explain]
|
|
39
|
+
@explain_user_id = context[:explain_user_id].to_s
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def permission_set = resource.permission_set
|
|
43
|
+
def key = row.name
|
|
44
|
+
def field = row.field
|
|
45
|
+
def label = row.label
|
|
46
|
+
def muting? = permission_set.is_muting
|
|
47
|
+
def level = row.level
|
|
48
|
+
|
|
49
|
+
# Locked when the field, its Resource or its Permission Set is (is_editable: false).
|
|
50
|
+
def locked? = !EditableLock.locked_record(field, resource).nil?
|
|
51
|
+
|
|
52
|
+
# The mask strategy the select starts on: the saved one, else the
|
|
53
|
+
# recommended one for a sensitive column, else 'full'.
|
|
54
|
+
def strategy
|
|
55
|
+
field&.mask_strategy || row.match&.strategy || DEFAULT_STRATEGY
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Other columns that follow this one (`_id` → `_uuid`, `_cents` → base name).
|
|
59
|
+
def variants
|
|
60
|
+
@variants ||= @configuration.field_variants_for(key)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# The edit level needs edit access on the Resource, except in a muting set.
|
|
64
|
+
def edit_disabled? = !muting? && !resource.edit_access
|
|
65
|
+
|
|
66
|
+
def in_force? = row.in_force?
|
|
67
|
+
|
|
68
|
+
# The note under the level control while no setting is in force, as the
|
|
69
|
+
# suffix of a locale key under `resource_fields.show` — or nil.
|
|
70
|
+
def unconfigured_note
|
|
71
|
+
return nil if row.in_force?
|
|
72
|
+
|
|
73
|
+
implicit = row.implicit_grant
|
|
74
|
+
if row.configured?
|
|
75
|
+
implicit ? :turned_off_implicit : :turned_off
|
|
76
|
+
else
|
|
77
|
+
implicit ? :not_configured_implicit : :not_configured
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Setting the note links to.
|
|
82
|
+
def unconfigured_setting
|
|
83
|
+
row.implicit_grant ? 'extra_fields_default_access' : 'unconfigured_field_strategy'
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# What happens to a field nobody set up (a key under `ui.unconfigured`).
|
|
87
|
+
def unconfigured_behaviour = @configuration.unconfigured_field_strategy
|
|
88
|
+
|
|
89
|
+
def sensitive? = row.sensitive?
|
|
90
|
+
|
|
91
|
+
# The privacy recommendation banner: only for a sensitive field in a granting set.
|
|
92
|
+
def recommendation_banner? = sensitive? && !muting?
|
|
93
|
+
|
|
94
|
+
# :applied (already masked with the recommended strategy), :apply (offer
|
|
95
|
+
# the button) or nil (locked: nothing to offer).
|
|
96
|
+
def recommendation
|
|
97
|
+
return :applied if level == 'masked' && strategy == row.match.strategy
|
|
98
|
+
return nil if locked?
|
|
99
|
+
|
|
100
|
+
:apply
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def virtual? = row.virtual
|
|
104
|
+
def declared? = row.declared
|
|
105
|
+
def extra_fields_method = @configuration.extra_fields_method
|
|
106
|
+
def model_masks_virtual? = rows.model&.masked_on_model?(key) || false
|
|
107
|
+
|
|
108
|
+
# The person the explain select starts on.
|
|
109
|
+
def explain_selected?(user) = @explain_user_id == user.id.to_s
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
# Maps the admin UI's single "access level" choice to ResourceField flags
|
|
6
|
+
# and back. Granting sets offer Hidden / Masked / Read-only / Editable;
|
|
7
|
+
# muting sets offer Unchanged / Force mask / Hide, where the same flags mean
|
|
8
|
+
# "remove" (see PermissionHashBuilder#mute_single_field).
|
|
9
|
+
module FieldLevel
|
|
10
|
+
GRANTING = {
|
|
11
|
+
'hidden' => { read_access: false, edit_access: false, mask_access: false },
|
|
12
|
+
'masked' => { read_access: true, edit_access: false, mask_access: true },
|
|
13
|
+
'read' => { read_access: true, edit_access: false, mask_access: false },
|
|
14
|
+
'edit' => { read_access: true, edit_access: true, mask_access: false }
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
MUTING = {
|
|
18
|
+
'leave' => { read_access: false, edit_access: false, mask_access: false },
|
|
19
|
+
'mask' => { read_access: false, edit_access: false, mask_access: true },
|
|
20
|
+
'hide' => { read_access: true, edit_access: true, mask_access: false }
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
# Ordered from most closed to most open (granting) — used to rank levels.
|
|
24
|
+
GRANTING_ORDER = %w[hidden masked read edit].freeze
|
|
25
|
+
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
def levels_for(muting:)
|
|
29
|
+
muting ? MUTING.keys : GRANTING.keys
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def flags_for(level, muting:)
|
|
33
|
+
(muting ? MUTING : GRANTING).fetch(level.to_s) { raise ArgumentError, "Unknown field level: #{level.inspect}" }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def valid?(level, muting:)
|
|
37
|
+
(muting ? MUTING : GRANTING).key?(level.to_s)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Derives the level from a field's flags. Flag combinations the UI never
|
|
41
|
+
# writes (e.g. edit + mask) collapse to the closest level.
|
|
42
|
+
def level_of(field, muting:)
|
|
43
|
+
read = field.read_access
|
|
44
|
+
edit = field.edit_access
|
|
45
|
+
mask = field.mask_access
|
|
46
|
+
return muting_level(read, edit, mask) if muting
|
|
47
|
+
|
|
48
|
+
return 'hidden' unless read
|
|
49
|
+
return 'masked' if mask
|
|
50
|
+
|
|
51
|
+
edit ? 'edit' : 'read'
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def muting_level(read, edit, mask)
|
|
55
|
+
return 'hide' if read || edit
|
|
56
|
+
|
|
57
|
+
mask ? 'mask' : 'leave'
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Level an engine field-permission entry resolves to
|
|
61
|
+
# ({ read_access:, edit_access:, mask_access: } from the permissions hash).
|
|
62
|
+
def effective_level(entry)
|
|
63
|
+
return 'hidden' if entry.nil? || !entry[:read_access]
|
|
64
|
+
return 'masked' if entry[:mask_access]
|
|
65
|
+
|
|
66
|
+
entry[:edit_access] ? 'edit' : 'read'
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def rank(level)
|
|
70
|
+
GRANTING_ORDER.index(level.to_s) || -1
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
# One row per field on a Resource page: configured ResourceFields, model
|
|
6
|
+
# columns nobody configured yet, and virtual fields — with the flags the
|
|
7
|
+
# UI needs (sensitive, virtual, column missing, level).
|
|
8
|
+
class FieldRows
|
|
9
|
+
# `declared` — a virtual field the model lists in extra_fields_method
|
|
10
|
+
# (a virtual row that is not declared was discovered). `implicit_grant` —
|
|
11
|
+
# no setting in force here (none, or one that is turned off), and
|
|
12
|
+
# ExtraFieldDefaults makes the field readable and editable right now
|
|
13
|
+
# (extra_fields_default_access = :grant).
|
|
14
|
+
Row = Struct.new(:name, :label, :field, :match, :virtual, :missing, :level, :declared, :implicit_grant,
|
|
15
|
+
keyword_init: true) do
|
|
16
|
+
def configured? = !field.nil?
|
|
17
|
+
def sensitive? = !match.nil?
|
|
18
|
+
def locked? = field && field.is_editable == false
|
|
19
|
+
def active? = field.nil? || field.is_active
|
|
20
|
+
|
|
21
|
+
# A setting the engine applies: it exists and is turned on. A row that
|
|
22
|
+
# is configured and not in force is turned off (is_active false); the
|
|
23
|
+
# engine skips it, so the field is treated like one with no setting.
|
|
24
|
+
def in_force? = configured? && active?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
GRANTING_FILTERS = %w[all hidden masked read edit sensitive unconfigured].freeze
|
|
28
|
+
MUTING_FILTERS = %w[all leave mask hide sensitive unconfigured].freeze
|
|
29
|
+
|
|
30
|
+
attr_reader :resource, :model
|
|
31
|
+
|
|
32
|
+
def initialize(resource, catalog: ModelCatalog.new, sensitive: SensitiveColumns.new)
|
|
33
|
+
@resource = resource
|
|
34
|
+
@muting = resource.permission_set.is_muting
|
|
35
|
+
@model = catalog.find(resource.name)
|
|
36
|
+
@sensitive = sensitive
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def filters
|
|
40
|
+
@muting ? MUTING_FILTERS : GRANTING_FILTERS
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def rows
|
|
44
|
+
@rows ||= (configured_rows + unconfigured_rows).sort_by { |r| [r.configured? ? 0 : 1, r.name] }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def filtered(filter: 'all', query: nil)
|
|
48
|
+
needle = query.to_s.strip.downcase
|
|
49
|
+
rows.select { |r| match_filter?(r, filter) && (needle.empty? || "#{r.name} #{r.label}".downcase.include?(needle)) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def counts
|
|
53
|
+
filters.index_with { |filter| rows.count { |r| match_filter?(r, filter) } }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def row(name)
|
|
57
|
+
rows.find { |r| r.name == name.to_s }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# --- sync with model -----------------------------------------------------
|
|
61
|
+
|
|
62
|
+
# Managed columns and virtual fields nobody configured in this resource.
|
|
63
|
+
def new_columns
|
|
64
|
+
return [] unless @model
|
|
65
|
+
|
|
66
|
+
@model.managed_field_names - configured_names
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def missing_fields
|
|
70
|
+
rows.select(&:missing)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def ignored_columns
|
|
74
|
+
Array(@model&.ignored_column_names)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def sync_needed?
|
|
78
|
+
new_columns.any? || missing_fields.any?
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
# Every row counts, switched off or not: a field with a row cannot get a
|
|
84
|
+
# second one.
|
|
85
|
+
def configured_names
|
|
86
|
+
@configured_names ||= @resource.resource_fields.map(&:name)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def configured_rows
|
|
90
|
+
@resource.resource_fields.map { |field| configured_row(field) }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def configured_row(field)
|
|
94
|
+
name = field.name
|
|
95
|
+
virtual = virtual?(name)
|
|
96
|
+
Row.new(name: name, label: field.label.presence || human(name), field: field, match: @sensitive.match(name),
|
|
97
|
+
virtual: virtual, missing: @model.present? && !virtual && @model.column_names.exclude?(name),
|
|
98
|
+
level: FieldLevel.level_of(field, muting: @muting), declared: declared?(name),
|
|
99
|
+
implicit_grant: !field.is_active && implicit_grant?(name))
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def unconfigured_rows
|
|
103
|
+
new_columns.map do |name|
|
|
104
|
+
Row.new(name: name, label: human(name), field: nil, match: @sensitive.match(name), virtual: virtual?(name),
|
|
105
|
+
missing: false, level: nil, declared: declared?(name), implicit_grant: implicit_grant?(name))
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def implicit_grant?(name)
|
|
110
|
+
@model.present? && @model.implicit_grant?(name)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# A field configured while discovery was on stays virtual (not "column
|
|
114
|
+
# missing") after discovery is turned off.
|
|
115
|
+
def virtual?(name)
|
|
116
|
+
@model.present? && @model.virtual_field?(name)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def declared?(name)
|
|
120
|
+
@model.present? && @model.declared_virtual_field_names.include?(name)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def human(name)
|
|
124
|
+
@model ? @model.klass.human_attribute_name(name) : name.to_s.humanize
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def match_filter?(row, filter)
|
|
128
|
+
case filter
|
|
129
|
+
when 'all' then true
|
|
130
|
+
when 'sensitive' then row.sensitive?
|
|
131
|
+
when 'unconfigured' then !row.configured?
|
|
132
|
+
else row.level == filter
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support/core_ext/numeric/time' # UNSEEN_AFTER, read as this file loads
|
|
4
|
+
|
|
5
|
+
module PermissionForge
|
|
6
|
+
module Admin
|
|
7
|
+
# The latest recorded change event of each of a list of records: who
|
|
8
|
+
# really made the last edit, and when. `updated_by` alone cannot say —
|
|
9
|
+
# an edit by nobody, by an HTTP Basic login or by an API client keeps the
|
|
10
|
+
# previous editor — so wherever the admin shows "last edited by", an
|
|
11
|
+
# event wins over the columns (see AuditTrail::Entry and the footer).
|
|
12
|
+
#
|
|
13
|
+
# Two queries per record type and slice: the newest event id per record
|
|
14
|
+
# (the ids grow with every insert), then those events.
|
|
15
|
+
class LastChanges
|
|
16
|
+
SLICE = 500
|
|
17
|
+
|
|
18
|
+
# @param records [Array<ActiveRecord::Base>] permission records
|
|
19
|
+
# @param events [Array<String>, nil] only these change events (e.g.
|
|
20
|
+
# %w[assignment.granted] — who granted it); nil: any change event
|
|
21
|
+
def initialize(records, events: nil)
|
|
22
|
+
@records = Array(records).compact
|
|
23
|
+
@events = events
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Edits this much later than the last event are edits the log did not see.
|
|
27
|
+
UNSEEN_AFTER = 1.second
|
|
28
|
+
|
|
29
|
+
# @return [AuditEvent, nil] the latest (matching) change event of the record
|
|
30
|
+
def [](record)
|
|
31
|
+
index[key(record.class.base_class.name, record.id)]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# The latest change event when it is the record's last edit; nil when
|
|
35
|
+
# the record was edited after it (updated_at more than UNSEEN_AFTER
|
|
36
|
+
# later — an edit made while auditing was off): the columns are newer.
|
|
37
|
+
#
|
|
38
|
+
# @return [AuditEvent, nil]
|
|
39
|
+
def last_edit(record)
|
|
40
|
+
event = self[record]
|
|
41
|
+
return nil unless event
|
|
42
|
+
|
|
43
|
+
updated = record.try(:updated_at)
|
|
44
|
+
updated && updated > event.occurred_at + UNSEEN_AFTER ? nil : event
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Whether the record is one of those this lookup was built for.
|
|
48
|
+
def covers?(record)
|
|
49
|
+
@keys ||= @records.to_set { |candidate| key(candidate.class.base_class.name, candidate.id) }
|
|
50
|
+
@keys.include?(key(record.class.base_class.name, record.id))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def key(type, id)
|
|
56
|
+
"#{type}##{id}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def index
|
|
60
|
+
@index ||= available? ? load_index : {}
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def available?
|
|
64
|
+
@records.any? && PermissionForge::Audit.enabled?
|
|
65
|
+
rescue StandardError
|
|
66
|
+
false
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def load_index
|
|
70
|
+
ids_by_type.each_with_object({}) do |(type, ids), out|
|
|
71
|
+
ids.each_slice(SLICE) { |slice| index_latest(out, type, slice) }
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def index_latest(index, type, ids)
|
|
76
|
+
latest(type, ids).each { |event| index[key(type, event.record_id)] = event }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# { record type => [ids as Strings] }
|
|
80
|
+
def ids_by_type
|
|
81
|
+
@records.group_by { |record| record.class.base_class.name }
|
|
82
|
+
.transform_values { |records| records.map { |record| record.id.to_s }.uniq }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def latest(type, ids)
|
|
86
|
+
events = PermissionForge::AuditEvent
|
|
87
|
+
matching = events.where(category: 'change', record_type: type, record_id: ids)
|
|
88
|
+
matching = matching.where(event: @events) if @events
|
|
89
|
+
newest = matching.group(:record_id).select(events.arel_table[:id].maximum)
|
|
90
|
+
events.where(id: newest).to_a
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
# Read-only reflection over the host app's ActiveRecord models, so the
|
|
6
|
+
# admin UI only ever offers real models, columns and associations (no
|
|
7
|
+
# free-text names that silently do nothing when misspelled).
|
|
8
|
+
class ModelCatalog
|
|
9
|
+
Column = Struct.new(:name, :label, :type, :enum_values, :user_reference, :virtual, keyword_init: true) do
|
|
10
|
+
def user_reference? = user_reference
|
|
11
|
+
def virtual? = virtual
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
Association = Struct.new(:name, :label, :class_name, :foreign_key, :macro, keyword_init: true) do
|
|
15
|
+
# @param reflection [ActiveRecord::Reflection::AssociationReflection] (non-polymorphic)
|
|
16
|
+
# @param target [Class] the reflection's resolved class
|
|
17
|
+
# @param owner [Class] the model declaring the association
|
|
18
|
+
def self.from(reflection, target, owner)
|
|
19
|
+
name = reflection.name
|
|
20
|
+
new(name: name.to_s, label: owner.human_attribute_name(name), class_name: target.name,
|
|
21
|
+
foreign_key: reflection.foreign_key.to_s, macro: reflection.macro)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# A model the admin can protect. Its virtual fields come from
|
|
26
|
+
# PermissionForge::VirtualFields.
|
|
27
|
+
class ModelInfo
|
|
28
|
+
attr_reader :klass
|
|
29
|
+
|
|
30
|
+
# Whether a field with no active ResourceField row gets the implicit
|
|
31
|
+
# read + edit entry from ExtraFieldDefaults right now.
|
|
32
|
+
delegate :implicit_grant?, to: :virtual_fields
|
|
33
|
+
|
|
34
|
+
def initialize(klass, configuration)
|
|
35
|
+
@klass = klass
|
|
36
|
+
@configuration = configuration
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def name = @klass.name
|
|
40
|
+
def label = @klass.model_name.human
|
|
41
|
+
def plural_label = @klass.model_name.human(count: 2)
|
|
42
|
+
|
|
43
|
+
def column_names
|
|
44
|
+
@column_names ||= @klass.column_names
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Columns PermissionForge manages (ignored_fields removed).
|
|
48
|
+
def managed_column_names
|
|
49
|
+
column_names - ignored_column_names
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def ignored_column_names
|
|
53
|
+
column_names & Array(@configuration.ignored_fields).map(&:to_s)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Virtual fields the model declares through extra_fields_method.
|
|
57
|
+
def declared_virtual_field_names
|
|
58
|
+
virtual_fields.declared
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Declared + discovered (config.virtual_field_discovery), never a
|
|
62
|
+
# column or an ignored field.
|
|
63
|
+
def virtual_field_names
|
|
64
|
+
virtual_fields.names
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Every field the admin configures: managed columns, then virtual fields.
|
|
68
|
+
def managed_field_names
|
|
69
|
+
managed_column_names + virtual_field_names
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def virtual_field?(name)
|
|
73
|
+
virtual_fields.include?(name)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Whether code that reads a virtual field straight from the model gets
|
|
77
|
+
# the field's level: its reader is wrapped on the class and
|
|
78
|
+
# config.mask_virtual_attributes selects the model.
|
|
79
|
+
def masked_on_model?(name)
|
|
80
|
+
PermissionForge::Concerns::MaskedAttributes::VirtualReaders.masked?(@klass, name)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def columns
|
|
84
|
+
@columns ||= managed_column_names.map { |name| column(name) } +
|
|
85
|
+
virtual_field_names.map { |name| virtual_column(name) }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def column(name)
|
|
89
|
+
name = name.to_s
|
|
90
|
+
return virtual_column(name) unless column_names.include?(name)
|
|
91
|
+
|
|
92
|
+
Column.new(name: name, label: @klass.human_attribute_name(name), type: column_type(name),
|
|
93
|
+
enum_values: enum_values(name), user_reference: user_reference?(name), virtual: false)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def associations
|
|
97
|
+
@associations ||= @klass.reflect_on_all_associations.filter_map { |ref| build_association(ref) }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def association(name)
|
|
101
|
+
associations.find { |association| association.name == name.to_s }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def virtual_fields
|
|
107
|
+
@virtual_fields ||= PermissionForge::VirtualFields.new(@klass, @configuration)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# nil for polymorphic or unresolvable associations.
|
|
111
|
+
def build_association(ref)
|
|
112
|
+
target = safe_klass(ref) unless ref.polymorphic?
|
|
113
|
+
Association.from(ref, target, @klass) if target
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def virtual_column(name)
|
|
117
|
+
Column.new(name: name, label: @klass.human_attribute_name(name), type: :string,
|
|
118
|
+
enum_values: nil, user_reference: false, virtual: true)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def column_type(name)
|
|
122
|
+
@klass.type_for_attribute(name).type || :string
|
|
123
|
+
rescue StandardError
|
|
124
|
+
:string
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def enum_values(name)
|
|
128
|
+
return nil unless @klass.respond_to?(:defined_enums)
|
|
129
|
+
|
|
130
|
+
@klass.defined_enums[name]&.keys
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def user_reference?(name)
|
|
134
|
+
user_class = @configuration.user_class.to_s
|
|
135
|
+
@klass.reflect_on_all_associations(:belongs_to).any? do |ref|
|
|
136
|
+
!ref.polymorphic? && ref.foreign_key.to_s == name && safe_klass(ref)&.name == user_class
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def safe_klass(ref)
|
|
141
|
+
ref.klass
|
|
142
|
+
rescue StandardError
|
|
143
|
+
nil
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def initialize(configuration: PermissionForge.configuration)
|
|
148
|
+
@configuration = configuration
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# @return [Array<ModelInfo>] sorted by label
|
|
152
|
+
def models
|
|
153
|
+
@models ||= model_classes.map { |k| ModelInfo.new(k, @configuration) }.sort_by(&:label)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# @return [ModelInfo, nil] for a model name, whether or not it is listed.
|
|
157
|
+
# One ModelInfo per name for this catalog, so its reflection runs once
|
|
158
|
+
# however many Permission Sets contain the model.
|
|
159
|
+
def find(name)
|
|
160
|
+
key = name.to_s
|
|
161
|
+
@found ||= {}
|
|
162
|
+
@found.fetch(key) { @found[key] = build_info(key) }
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
private
|
|
166
|
+
|
|
167
|
+
def build_info(name)
|
|
168
|
+
klass = name.safe_constantize
|
|
169
|
+
ModelInfo.new(klass, @configuration) if usable?(klass)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def model_classes
|
|
173
|
+
classes = if @configuration.admin_model_resolver
|
|
174
|
+
Array(@configuration.admin_model_resolver.call)
|
|
175
|
+
else
|
|
176
|
+
default_model_classes
|
|
177
|
+
end
|
|
178
|
+
classes.select { |k| usable?(k) }.uniq
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def default_model_classes
|
|
182
|
+
eager_load_app_models
|
|
183
|
+
ActiveRecord::Base.descendants.select { |k| k.name.present? && app_model?(k) }
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def usable?(klass)
|
|
187
|
+
klass.is_a?(Class) && klass < ActiveRecord::Base && !klass.abstract_class? &&
|
|
188
|
+
!klass.name.to_s.start_with?('PermissionForge::') && klass.table_exists?
|
|
189
|
+
rescue StandardError
|
|
190
|
+
false
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Only models defined inside the host app (not gems/frameworks).
|
|
194
|
+
def app_model?(klass)
|
|
195
|
+
return true unless defined?(Rails) && Rails.root
|
|
196
|
+
|
|
197
|
+
file = Object.const_source_location(klass.name)&.first
|
|
198
|
+
file.nil? || file.start_with?(Rails.root.to_s)
|
|
199
|
+
rescue StandardError
|
|
200
|
+
true
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def eager_load_app_models
|
|
204
|
+
return unless defined?(Rails) && Rails.root && Rails.respond_to?(:autoloaders)
|
|
205
|
+
|
|
206
|
+
eager_load_dir(Rails.autoloaders.main, Rails.root.join('app', 'models').to_s)
|
|
207
|
+
rescue StandardError => e
|
|
208
|
+
Rails.logger&.debug("[PermissionForge] model eager load skipped: #{e.class}")
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def eager_load_dir(loader, dir)
|
|
212
|
+
loader.eager_load_dir(dir) if File.directory?(dir) && loader.respond_to?(:eager_load_dir)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|