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,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
# Uniform filter / search / paging over the people list, whether
|
|
6
|
+
# config.user_list returns an ActiveRecord relation (filtered in SQL) or an
|
|
7
|
+
# array (filtered in Ruby). Each operation returns a new candidates object.
|
|
8
|
+
module PeopleCandidates
|
|
9
|
+
def self.wrap(list, configuration:)
|
|
10
|
+
list.respond_to?(:where) ? Relation.new(list) : List.new(list, configuration)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# SQL-backed: searches whichever of the usual name/email columns exist.
|
|
14
|
+
class Relation
|
|
15
|
+
# Common person columns; only those the user table has are searched.
|
|
16
|
+
SEARCH_COLUMNS = %w[name email full_name display_name first_name last_name firstname lastname
|
|
17
|
+
username login].freeze
|
|
18
|
+
# A query of digits also matches the id — up to a bigint's length.
|
|
19
|
+
NUMERIC = /\A\d{1,18}\z/
|
|
20
|
+
|
|
21
|
+
def initialize(relation)
|
|
22
|
+
@relation = relation
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def only(ids)
|
|
26
|
+
Relation.new(@relation.where(id: ids))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def excluding(ids)
|
|
30
|
+
Relation.new(@relation.where.not(id: ids))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def search(query)
|
|
34
|
+
columns = SEARCH_COLUMNS & @relation.klass.column_names
|
|
35
|
+
id = query.to_i if query.match?(NUMERIC)
|
|
36
|
+
return Relation.new(@relation.none) if columns.empty? && id.nil?
|
|
37
|
+
|
|
38
|
+
Relation.new(@relation.where(word_conditions(columns, query.downcase, id)))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def count
|
|
42
|
+
@relation.except(:order, :limit, :offset).count
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def take(offset, limit)
|
|
46
|
+
@relation.offset(offset).limit(limit).to_a
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
# Every word must match some column, so "ana kras" finds Ana Krasniqi
|
|
52
|
+
# whether the name lives in one column or in first/last name columns.
|
|
53
|
+
# A numeric query also matches the id.
|
|
54
|
+
def word_conditions(columns, needle, id)
|
|
55
|
+
words = needle.split
|
|
56
|
+
matched = words.map { |word| any_column(columns, word) }.compact.reduce(:and)
|
|
57
|
+
return table[:id].eq(id) if matched.nil?
|
|
58
|
+
|
|
59
|
+
id ? matched.or(table[:id].eq(id)) : matched
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def any_column(columns, word)
|
|
63
|
+
like = "%#{ActiveRecord::Base.sanitize_sql_like(word)}%"
|
|
64
|
+
columns.map { |column| table[column].lower.matches(like) }.reduce(:or)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def table
|
|
68
|
+
@relation.klass.arel_table
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Array-backed (custom resolvers): searches the person's display label.
|
|
73
|
+
class List
|
|
74
|
+
def initialize(people, configuration)
|
|
75
|
+
@people = people
|
|
76
|
+
@configuration = configuration
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def only(ids)
|
|
80
|
+
List.new(@people.select { |u| ids.include?(u.id) }, @configuration)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def excluding(ids)
|
|
84
|
+
List.new(@people.reject { |u| ids.include?(u.id) }, @configuration)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def search(query)
|
|
88
|
+
needle = query.downcase
|
|
89
|
+
List.new(@people.select { |u| label_text(u).include?(needle) }, @configuration)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def count
|
|
93
|
+
@people.size
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def take(offset, limit)
|
|
97
|
+
Array(@people)[offset, limit] || []
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def label_text(user)
|
|
103
|
+
PersonLabel.new(user, configuration: @configuration).search_text
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
# Searches and filters the people shown on the People page and in pickers.
|
|
6
|
+
# The candidate list comes from config.user_list_resolver (or
|
|
7
|
+
# user_model.order(:id)); search runs in SQL on whichever of the usual
|
|
8
|
+
# name/email columns exist, or in Ruby for array-returning resolvers
|
|
9
|
+
# (see PeopleCandidates).
|
|
10
|
+
class PeopleDirectory
|
|
11
|
+
FILTERS = %w[all ending expired muting no_access].freeze
|
|
12
|
+
ENDING_WINDOW_DAYS = 7
|
|
13
|
+
DEFAULT_PER_PAGE = 25
|
|
14
|
+
# Beyond this the offset overflows the database's integer; the page is
|
|
15
|
+
# empty long before.
|
|
16
|
+
MAX_PAGE = (2**31) - 1
|
|
17
|
+
# Filters that keep everyone *except* the matching people.
|
|
18
|
+
EXCLUDING_FILTERS = %w[no_access].freeze
|
|
19
|
+
|
|
20
|
+
# Assignments whose people a filter keeps ('no_access' keeps everyone else).
|
|
21
|
+
FILTER_ASSIGNMENTS = {
|
|
22
|
+
'ending' => lambda {
|
|
23
|
+
PermissionForge::PermissionSetUser.where(expires_at: Time.current..ENDING_WINDOW_DAYS.days.from_now.end_of_day)
|
|
24
|
+
},
|
|
25
|
+
'expired' => -> { PermissionForge::PermissionSetUser.expired },
|
|
26
|
+
'muting' => lambda {
|
|
27
|
+
PermissionForge::PermissionSetUser.active_assignments.where(permission_set_id: PermissionForge::PermissionSet.muting.select(:id))
|
|
28
|
+
},
|
|
29
|
+
'no_access' => lambda {
|
|
30
|
+
PermissionForge::PermissionSetUser.active_assignments.where(permission_set_id: PermissionForge::PermissionSet.active.select(:id))
|
|
31
|
+
}
|
|
32
|
+
}.freeze
|
|
33
|
+
|
|
34
|
+
Page = Struct.new(:people, :total, :page, :per_page, keyword_init: true) do
|
|
35
|
+
def more? = total > page * per_page
|
|
36
|
+
def shown = [total, page * per_page].min
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def initialize(configuration: PermissionForge.configuration)
|
|
40
|
+
@configuration = configuration
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Typed or pasted text, as it is searched for people: without NUL bytes
|
|
44
|
+
# (the search builds quoted literals, and a NUL would cut the SQL
|
|
45
|
+
# statement short) and without surrounding white space. The one input
|
|
46
|
+
# boundary for people search and the bulk matcher.
|
|
47
|
+
#
|
|
48
|
+
# @return [String]
|
|
49
|
+
def self.search_text(text)
|
|
50
|
+
text.to_s.delete("\0").strip
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Paging options: page: (1-based, default 1), per_page: (default 25).
|
|
54
|
+
# @return [Page]
|
|
55
|
+
def search(query: nil, filter: 'all', **paging)
|
|
56
|
+
filter = 'all' unless FILTERS.include?(filter.to_s)
|
|
57
|
+
candidates = apply_query(apply_filter(base_candidates, filter.to_s), query)
|
|
58
|
+
paginate(candidates, paging[:page].to_s.to_i.clamp(1, MAX_PAGE), paging.fetch(:per_page, DEFAULT_PER_PAGE))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Counts for the filter chips (respecting the search query).
|
|
62
|
+
def counts(query: nil)
|
|
63
|
+
base = apply_query(base_candidates, query)
|
|
64
|
+
FILTERS.index_with { |filter| apply_filter(base, filter).count }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# People matching a query, capped (for the person picker).
|
|
68
|
+
def pick(query:, limit: nil)
|
|
69
|
+
limit ||= @configuration.user_list_limit || 500
|
|
70
|
+
candidates = apply_query(base_candidates, query)
|
|
71
|
+
total = candidates.count
|
|
72
|
+
Page.new(people: candidates.take(0, limit), total: total, page: 1, per_page: limit)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# The person `id` names, looked up within config.user_list
|
|
76
|
+
# (user_list_resolver): the one lookup of a person by id for the admin
|
|
77
|
+
# and the API. Someone the resolver leaves out (another tenant's user,
|
|
78
|
+
# an account the host keeps out of the admin) is nobody here: nil,
|
|
79
|
+
# which callers answer with a 404 / "not found".
|
|
80
|
+
#
|
|
81
|
+
# @param id [String, Integer] anything else (a Hash, an Array) names nobody
|
|
82
|
+
# @return [Object, nil] the user
|
|
83
|
+
def find(id)
|
|
84
|
+
find_all([id]).first
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# The people `ids` name within config.user_list; ids that name nobody
|
|
88
|
+
# there (or are not keys at all) are dropped.
|
|
89
|
+
#
|
|
90
|
+
# @param ids [Array, String, Integer]
|
|
91
|
+
# @return [Array] users
|
|
92
|
+
def find_all(ids)
|
|
93
|
+
ids = Ids.cast(ids, configuration: @configuration)
|
|
94
|
+
ids.empty? ? [] : base_candidates.only(ids).take(0, ids.size)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Request values cast to the user model's primary-key type. Only a
|
|
98
|
+
# String or an Integer can name a person; for an integer key only
|
|
99
|
+
# digits do (the database would read '1abc' as 1), and never past a
|
|
100
|
+
# bigint. A NUL byte names nobody.
|
|
101
|
+
module Ids
|
|
102
|
+
INTEGER_KEY = /\A\d{1,19}\z/
|
|
103
|
+
MAX_INTEGER_KEY = (2**63) - 1
|
|
104
|
+
|
|
105
|
+
# @return [Array] distinct cast ids, in order
|
|
106
|
+
def self.cast(ids, configuration: PermissionForge.configuration)
|
|
107
|
+
model = configuration.user_model
|
|
108
|
+
type = model.type_for_attribute(model.primary_key)
|
|
109
|
+
(ids.is_a?(Array) ? ids : [ids]).filter_map { |id| cast_one(id, type) }.uniq
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def self.cast_one(id, type)
|
|
113
|
+
return nil unless id.is_a?(String) || id.is_a?(Integer)
|
|
114
|
+
|
|
115
|
+
text = id.to_s.strip
|
|
116
|
+
return nil if text.empty? || text.include?("\0")
|
|
117
|
+
return nil if type.type == :integer && !integer_key?(text)
|
|
118
|
+
|
|
119
|
+
type.cast(text)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def self.integer_key?(text)
|
|
123
|
+
text.match?(INTEGER_KEY) && text.to_i <= MAX_INTEGER_KEY
|
|
124
|
+
end
|
|
125
|
+
private_class_method :cast_one, :integer_key?
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
private
|
|
129
|
+
|
|
130
|
+
# Memoized: an array-returning resolver is evaluated once per directory
|
|
131
|
+
# (the bulk matcher asks once per pasted line).
|
|
132
|
+
def base_candidates
|
|
133
|
+
@base_candidates ||= PeopleCandidates.wrap(@configuration.user_list, configuration: @configuration)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def apply_filter(candidates, filter)
|
|
137
|
+
ids = filter_ids(filter)
|
|
138
|
+
return candidates if ids.nil?
|
|
139
|
+
|
|
140
|
+
candidates.public_send(EXCLUDING_FILTERS.include?(filter) ? :excluding : :only, ids)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def filter_ids(filter)
|
|
144
|
+
FILTER_ASSIGNMENTS[filter]&.call&.select(:user_id)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def apply_query(candidates, query)
|
|
148
|
+
query = self.class.search_text(query)
|
|
149
|
+
query.empty? ? candidates : candidates.search(query)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def paginate(candidates, page, per_page)
|
|
153
|
+
Page.new(people: candidates.take((page - 1) * per_page, per_page), total: candidates.count, page: page,
|
|
154
|
+
per_page: per_page)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
# Creates Permission Sets from the admin UI: fresh, started from another
|
|
6
|
+
# set, or duplicated. Copies are deep (resources, fields, actions, rules,
|
|
7
|
+
# conditions) — never assignments — with every copied record unlocked.
|
|
8
|
+
module PermissionSetFactory
|
|
9
|
+
# @param attrs [Hash] name:, is_muting:, permission_set_group:
|
|
10
|
+
# @param source [PermissionSet, nil] set to start from
|
|
11
|
+
def self.create(attrs, source: nil)
|
|
12
|
+
return clone_unlocked(source, attrs.merge(is_active: true)) if source
|
|
13
|
+
|
|
14
|
+
PermissionForge::PermissionSet.create!(attrs.merge(is_active: true, is_editable: true))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# An editable copy. A source in a locked Permission Set Group is copied
|
|
18
|
+
# outside it: the copy would change the locked group's list of sets.
|
|
19
|
+
def self.duplicate(source, name:)
|
|
20
|
+
attrs = { name: name, is_editable: true }
|
|
21
|
+
attrs[:permission_set_group] = nil if EditableLock.locked?(source.permission_set_group)
|
|
22
|
+
clone_unlocked(source, attrs)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Case-insensitive.
|
|
26
|
+
def self.name_taken?(name)
|
|
27
|
+
PermissionForge::PermissionSet.where('LOWER(name) = ?', name.downcase).exists?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# `base`, else "`base` 2", "`base` 3", ... — the first free name.
|
|
31
|
+
def self.unique_name(base)
|
|
32
|
+
candidate = base
|
|
33
|
+
suffix = 2
|
|
34
|
+
while name_taken?(candidate)
|
|
35
|
+
candidate = "#{base} #{suffix}"
|
|
36
|
+
suffix += 1
|
|
37
|
+
end
|
|
38
|
+
candidate
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Every copied row is created unlocked, so its create event holds the
|
|
42
|
+
# state it has (no second, unaudited write).
|
|
43
|
+
def self.clone_unlocked(source, attrs)
|
|
44
|
+
PermissionForge::Services::PermissionSetCloner.call(permission_set: source, attrs: attrs.merge(is_editable: true), unlock: true)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
# Display name / email / initials for a host-app user in the admin UI.
|
|
6
|
+
#
|
|
7
|
+
# Honors config.user_label_formatter for the name. Otherwise tries :name,
|
|
8
|
+
# :full_name, :display_name, then composes first_name/last_name (or firstname/lastname). Attribute
|
|
9
|
+
# reads go raw-column-first (read_attribute_before_type_cast) so
|
|
10
|
+
# MaskedAttributes on the User model never garbles admin lists; virtual
|
|
11
|
+
# attributes and blank columns fall back to the public reader.
|
|
12
|
+
class PersonLabel
|
|
13
|
+
NAME_METHODS = %i[name full_name display_name].freeze
|
|
14
|
+
|
|
15
|
+
def initialize(user, configuration: PermissionForge.configuration)
|
|
16
|
+
@user = user
|
|
17
|
+
@configuration = configuration
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def name
|
|
21
|
+
@name ||= formatted_name || default_name || email || fallback
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def email
|
|
25
|
+
@email ||= raw_value(:email).to_s.strip.presence
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Email shown under the name — omitted when the name already is the email.
|
|
29
|
+
def secondary
|
|
30
|
+
email unless email == name
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def initials
|
|
34
|
+
initial_letters(name.to_s.scan(/[[:alnum:]]+/)).join.upcase.presence || '?'
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Lower-cased "name email" for in-memory search.
|
|
38
|
+
def search_text
|
|
39
|
+
"#{name} #{email}".downcase
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def first_name
|
|
43
|
+
name.to_s.split(/\s+/).first.presence || name
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
# First letters of the first and last word, or the first two letters of a single word.
|
|
49
|
+
def initial_letters(words)
|
|
50
|
+
words.size > 1 ? [words.first[0], words.last[0]] : [words.first.to_s[0, 2]]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def formatted_name
|
|
54
|
+
formatter = @configuration.user_label_formatter
|
|
55
|
+
return nil unless formatter
|
|
56
|
+
|
|
57
|
+
formatter.call(@user).to_s.strip.presence
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def default_name
|
|
61
|
+
NAME_METHODS.each do |method|
|
|
62
|
+
value = raw_value(method).to_s.strip
|
|
63
|
+
return value unless value.empty?
|
|
64
|
+
end
|
|
65
|
+
composed(:first_name, :last_name) || composed(:firstname, :lastname)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def composed(first, last)
|
|
69
|
+
[raw_value(first), raw_value(last)].compact.map { |part| part.to_s.strip }.reject(&:empty?).join(' ').presence
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def fallback
|
|
73
|
+
I18n.t('permission_forge.shared.user_fallback', id: @user.id)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def raw_value(attr)
|
|
77
|
+
attr_name = attr.to_s
|
|
78
|
+
if readable_column?(attr_name)
|
|
79
|
+
value = @user.read_attribute_before_type_cast(attr_name)
|
|
80
|
+
return value if value.to_s.strip.present?
|
|
81
|
+
end
|
|
82
|
+
@user.try(attr)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# True when attr_name is a loaded column on an AR-like user (guards
|
|
86
|
+
# partial SELECTs from a custom user_list_resolver).
|
|
87
|
+
def readable_column?(attr_name)
|
|
88
|
+
klass = @user.class
|
|
89
|
+
klass.respond_to?(:column_names) && klass.column_names.include?(attr_name) &&
|
|
90
|
+
@user.respond_to?(:read_attribute_before_type_cast) &&
|
|
91
|
+
(!@user.respond_to?(:has_attribute?) || @user.has_attribute?(attr_name))
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
# The id of a PermissionForge record, from a request value: an Integer, or
|
|
6
|
+
# nil for anything that is not one — blank, a Hash, an Array, text, or more
|
|
7
|
+
# digits than a bigint holds (the database would raise, or read '1abc' as 1).
|
|
8
|
+
module RecordId
|
|
9
|
+
FORMAT = /\A\d{1,18}\z/
|
|
10
|
+
|
|
11
|
+
# @param value [Object]
|
|
12
|
+
# @return [Integer, nil]
|
|
13
|
+
def self.cast(value)
|
|
14
|
+
text = value.to_s.strip if value.is_a?(String) || value.is_a?(Integer)
|
|
15
|
+
text.to_i if text&.match?(FORMAT)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
# What the rule builder page (permission_set_rules/_builder) decides beyond
|
|
6
|
+
# the RuleForm's own state: which notes show, which option is selected in
|
|
7
|
+
# each select, and how the "how to combine" choices read. The template only
|
|
8
|
+
# draws it.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# builder = RuleBuilder.new(form: form, resource: resource, model_errors: [])
|
|
12
|
+
# builder.ignored_warning? # => false
|
|
13
|
+
# builder.join_choices.map(&:join) # => ['auto', 'joins', 'left_joins']
|
|
14
|
+
class RuleBuilder
|
|
15
|
+
# join: the value; checked: the stored choice; auto: the one that is
|
|
16
|
+
# automatic (its title is named in the "auto" description).
|
|
17
|
+
JoinChoice = Struct.new(:join, :checked, :auto, keyword_init: true) do
|
|
18
|
+
def auto? = join == 'auto'
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
attr_reader :form, :resource, :model_errors
|
|
22
|
+
|
|
23
|
+
def initialize(form:, resource:, model_errors: [], configuration: PermissionForge.configuration)
|
|
24
|
+
@form = form
|
|
25
|
+
@resource = resource
|
|
26
|
+
@model_errors = Array(model_errors)
|
|
27
|
+
@configuration = configuration
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def errors = form.errors
|
|
31
|
+
def error_codes = errors.values
|
|
32
|
+
|
|
33
|
+
def steps = form.path_steps
|
|
34
|
+
def target = form.target_model
|
|
35
|
+
|
|
36
|
+
# A Resource that sees everything makes every rule moot.
|
|
37
|
+
def ignored_warning? = resource.view_all ? true : false
|
|
38
|
+
|
|
39
|
+
# The "follow a relationship" select: only while the target has any.
|
|
40
|
+
def follow_options = target ? target.associations : []
|
|
41
|
+
def follow? = follow_options.any?
|
|
42
|
+
|
|
43
|
+
def model_missing? = target.nil?
|
|
44
|
+
|
|
45
|
+
# Open the advanced panel when the join is not the automatic one.
|
|
46
|
+
def advanced_open? = form.join != 'auto'
|
|
47
|
+
|
|
48
|
+
def join_choices
|
|
49
|
+
RuleForm::JOINS.map { |join| JoinChoice.new(join: join, checked: form.join == join, auto: form.auto_join) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def scoping_on? = @configuration.auto_query_scoping.to_s == 'filter_only'
|
|
53
|
+
|
|
54
|
+
def field_selected?(column) = column.name == form.field_name
|
|
55
|
+
def operator_selected?(operator) = operator == form.operator
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
# Form object behind the rule builder: turns the sentence-style inputs
|
|
6
|
+
# (relationship path, field, operator, adaptive value, "the current user",
|
|
7
|
+
# include empty, join choice) into PermissionSetRule attributes, and lists
|
|
8
|
+
# the choices offered at each step from model reflection.
|
|
9
|
+
class RuleForm
|
|
10
|
+
ARITY = {
|
|
11
|
+
'equal' => :one, 'not_equal' => :one, 'less_than' => :one, 'greater_than' => :one,
|
|
12
|
+
'less_than_or_equal' => :one, 'greater_than_or_equal' => :one, 'in' => :list, 'not_in' => :list,
|
|
13
|
+
'contains' => :one, 'not_contains' => :one, 'starts_with' => :one, 'ends_with' => :one,
|
|
14
|
+
'between' => :range, 'is_null' => :none, 'is_not_null' => :none
|
|
15
|
+
}.freeze
|
|
16
|
+
USER_OPERATORS = %w[equal not_equal in not_in is_null is_not_null].freeze
|
|
17
|
+
NULLABLE = %w[equal not_equal in not_in].freeze
|
|
18
|
+
JOINS = %w[auto joins left_joins].freeze
|
|
19
|
+
CURRENT_USER = 'current_user'
|
|
20
|
+
VALUE_ERRORS = { one: :value, list: :list, range: :range }.freeze
|
|
21
|
+
|
|
22
|
+
attr_reader :name, :path, :field_name, :operator, :value, :values, :value_from, :value_to,
|
|
23
|
+
:current_user, :include_nulls, :failure_message, :join, :is_active, :errors
|
|
24
|
+
|
|
25
|
+
def initialize(resource:, params: nil, rule: nil, catalog: ModelCatalog.new)
|
|
26
|
+
@resource = resource
|
|
27
|
+
@catalog = catalog
|
|
28
|
+
@errors = {}
|
|
29
|
+
if params
|
|
30
|
+
assign_inputs(RuleFormInput.from_params(normalize(params)))
|
|
31
|
+
@operator = 'equal' unless operators.include?(@operator)
|
|
32
|
+
else
|
|
33
|
+
assign_inputs(RuleFormInput.from_rule(rule))
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# --- choices ----------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
def root_model
|
|
40
|
+
@root_model ||= @catalog.find(@resource.name)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# [[ModelInfo, Association selected or nil], ...] for each path step, plus
|
|
44
|
+
# the model the field is read from.
|
|
45
|
+
def path_steps
|
|
46
|
+
model = root_model
|
|
47
|
+
steps = []
|
|
48
|
+
@path.each do |assoc_name|
|
|
49
|
+
break unless model
|
|
50
|
+
|
|
51
|
+
assoc = model.association(assoc_name)
|
|
52
|
+
steps << [model, assoc]
|
|
53
|
+
model = assoc && @catalog.find(assoc.class_name)
|
|
54
|
+
end
|
|
55
|
+
@target_model = model
|
|
56
|
+
steps
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def target_model
|
|
60
|
+
path_steps unless defined?(@target_model)
|
|
61
|
+
@target_model
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def field_options
|
|
65
|
+
target_model ? target_model.columns.reject(&:virtual?) : []
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def column
|
|
69
|
+
field_options.find { |option| option.name == @field_name }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def operators
|
|
73
|
+
column&.user_reference? ? USER_OPERATORS : ARITY.keys
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def arity
|
|
77
|
+
ARITY.fetch(@operator, :one)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def nullable?
|
|
81
|
+
NULLABLE.include?(@operator)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def user_field?
|
|
85
|
+
column&.user_reference? || false
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def enum_values
|
|
89
|
+
column&.enum_values
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def input_type
|
|
93
|
+
case column&.type
|
|
94
|
+
when :integer, :decimal, :float, :bigint then 'number'
|
|
95
|
+
when :date then 'date'
|
|
96
|
+
when :datetime, :timestamp then 'datetime-local'
|
|
97
|
+
else 'text'
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def follows_relationship?
|
|
102
|
+
@path.any?
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# --- persistence --------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
def valid?
|
|
108
|
+
@errors = {}
|
|
109
|
+
@errors[:name] = :name_blank if @name.blank?
|
|
110
|
+
@errors[:field] = :field if column.nil?
|
|
111
|
+
validate_value
|
|
112
|
+
@errors.empty?
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def attributes
|
|
116
|
+
{
|
|
117
|
+
name: @name, field_name: @field_name, operator: @operator, field_value: field_value,
|
|
118
|
+
field_accessor_path: @path.any? ? @path.join('.') : nil, join_type: join_type,
|
|
119
|
+
include_nulls: nullable? && @include_nulls, failure_message: @failure_message.presence,
|
|
120
|
+
is_active: @is_active
|
|
121
|
+
}
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def field_value
|
|
125
|
+
case arity
|
|
126
|
+
when :none then nil
|
|
127
|
+
when :list then @values.join(',')
|
|
128
|
+
when :range then [@value_from, @value_to].join(',')
|
|
129
|
+
else @current_user ? CURRENT_USER : @value
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Rules without a relationship are filter rules (join_type nil).
|
|
134
|
+
def join_type
|
|
135
|
+
return nil if @path.empty?
|
|
136
|
+
return @join if %w[joins left_joins].include?(@join)
|
|
137
|
+
|
|
138
|
+
@include_nulls ? 'left_joins' : 'joins'
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def auto_join
|
|
142
|
+
@include_nulls ? 'left_joins' : 'joins'
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Unsaved rule for previews.
|
|
146
|
+
def preview_rule
|
|
147
|
+
PermissionForge::PermissionSetRule.new(attributes.merge(resource: @resource))
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
private
|
|
151
|
+
|
|
152
|
+
def normalize(params)
|
|
153
|
+
(params.respond_to?(:to_unsafe_h) ? params.to_unsafe_h : params.to_h).deep_stringify_keys
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Sets the reader ivars (@name, @path, ...) from RuleFormInput.
|
|
157
|
+
def assign_inputs(inputs)
|
|
158
|
+
inputs.each { |key, value| instance_variable_set(:"@#{key}", value) }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def validate_value
|
|
162
|
+
@errors[:value] = VALUE_ERRORS[arity] if value_missing?
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def value_missing?
|
|
166
|
+
case arity
|
|
167
|
+
when :one then !@current_user && @value.blank?
|
|
168
|
+
when :list then @values.empty?
|
|
169
|
+
when :range then @value_from.blank? || @value_to.blank?
|
|
170
|
+
else false
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|