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,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
module ChangeSetParts
|
|
6
|
+
# Edits to one resource: label/description, then CRUD/scope flags with
|
|
7
|
+
# the Read/Edit cascades (skipped for muting sets — inverted semantics).
|
|
8
|
+
class ResourceEdit
|
|
9
|
+
FLAGS = %w[read_access create_access edit_access delete_access view_all modify_all is_active].freeze
|
|
10
|
+
TEXT = %w[label description].freeze
|
|
11
|
+
READ_DEPENDENT = %w[edit_access delete_access view_all modify_all].freeze
|
|
12
|
+
|
|
13
|
+
def initialize(resource, muting:)
|
|
14
|
+
@resource = resource
|
|
15
|
+
@muting = muting
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def changes(attrs)
|
|
19
|
+
text_changes(attrs.slice(*TEXT)) + flag_changes(attrs.slice(*FLAGS).transform_values { |v| Values.truthy?(v) })
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def text_changes(texts)
|
|
25
|
+
texts.filter_map { |attr, value| change(attr, value.to_s.strip.presence, kind: :resource_text).assign }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# A flag is "cascaded" when its new value differs from (or was not
|
|
29
|
+
# part of) the request.
|
|
30
|
+
def flag_changes(requested)
|
|
31
|
+
wanted = wanted_flags(requested)
|
|
32
|
+
FLAGS.filter_map do |flag|
|
|
33
|
+
to = wanted[flag]
|
|
34
|
+
change(flag, to, kind: :resource_flag, cascaded: requested.fetch(flag, :absent) != to).assign
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def wanted_flags(requested)
|
|
39
|
+
wanted = FLAGS.to_h { |flag| [flag, requested.fetch(flag) { @resource.public_send(flag) }] }
|
|
40
|
+
cascade!(wanted) unless @muting
|
|
41
|
+
wanted
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def cascade!(flags)
|
|
45
|
+
READ_DEPENDENT.each { |flag| flags[flag] = false } unless flags['read_access']
|
|
46
|
+
flags['modify_all'] = false unless flags['edit_access'] && flags['view_all']
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def change(attribute, to, **attrs)
|
|
50
|
+
ChangeSet::Change.new(record: @resource, resource: @resource, attribute: attribute, to: to, **attrs)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
module ChangeSetParts
|
|
6
|
+
# Resource label/description and CRUD/scope flags (see ResourceEdit).
|
|
7
|
+
class ResourceFlags < Base
|
|
8
|
+
def call
|
|
9
|
+
section('resources').flat_map do |id, attrs|
|
|
10
|
+
resource = unlocked_resource(id)
|
|
11
|
+
next [] unless resource && attrs.is_a?(Hash)
|
|
12
|
+
|
|
13
|
+
ResourceEdit.new(resource, muting: muting?).changes(attrs)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
module ChangeSetParts
|
|
6
|
+
# Set header: name, description, active, group.
|
|
7
|
+
class SetAttrs < Base
|
|
8
|
+
SET_ATTRS = %w[name description is_active permission_set_group_id].freeze
|
|
9
|
+
# A group id that is neither blank (no group) nor digits.
|
|
10
|
+
NOT_A_GROUP = Object.new.freeze
|
|
11
|
+
TEXT_CAST = ->(value) { value.to_s.strip.presence if value.is_a?(String) || value.is_a?(Numeric) }
|
|
12
|
+
CASTS = {
|
|
13
|
+
'is_active' => ->(value) { Values.truthy?(value) },
|
|
14
|
+
'permission_set_group_id' => lambda { |value|
|
|
15
|
+
next nil if value.blank?
|
|
16
|
+
|
|
17
|
+
RecordId.cast(value) || NOT_A_GROUP
|
|
18
|
+
}
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
def call
|
|
22
|
+
section('set').slice(*SET_ATTRS).filter_map do |attr, value|
|
|
23
|
+
to = cast(attr, value)
|
|
24
|
+
next if to.equal?(NOT_A_GROUP)
|
|
25
|
+
next if attr == 'permission_set_group_id' && locked_group_move?(to)
|
|
26
|
+
|
|
27
|
+
assign_change(kind: :set_attr, record: permission_set, attribute: attr, to: to)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# A move into or out of a locked Permission Set Group is skipped
|
|
34
|
+
# (the controller refuses it first).
|
|
35
|
+
def locked_group_move?(group_id)
|
|
36
|
+
EditableLock.locked_group_for_move(from: permission_set.permission_set_group_id, to: group_id).present?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def cast(attr, value)
|
|
40
|
+
CASTS.fetch(attr, TEXT_CAST).call(value)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
module ChangeSetParts
|
|
6
|
+
# Stateless value rules shared by the ChangeSet collaborators.
|
|
7
|
+
module Values
|
|
8
|
+
def self.truthy?(value)
|
|
9
|
+
ActiveModel::Type::Boolean.new.cast(value) == true
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Edit on a resource without Edit is downgraded to Read (muting sets
|
|
13
|
+
# have inverted semantics, so Edit is always available there).
|
|
14
|
+
def self.effective_level(level, resource, muting:)
|
|
15
|
+
level == 'edit' && !(muting || resource.edit_access) ? 'read' : level
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
# Creates ResourceFields for model columns and virtual fields at one level,
|
|
6
|
+
# applying privacy defaults: sensitive names start Masked when gdpr_defaults
|
|
7
|
+
# is on, and the muting 'hide_sensitive' level hides only sensitive names.
|
|
8
|
+
class ColumnFields
|
|
9
|
+
def initialize(resource, muting:, sensitive: SensitiveColumns.new, configuration: PermissionForge.configuration)
|
|
10
|
+
@resource = resource
|
|
11
|
+
@muting = muting
|
|
12
|
+
@sensitive = sensitive
|
|
13
|
+
@configuration = configuration
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @return [Integer] number of fields created (columns whose level resolves to nil are skipped)
|
|
17
|
+
def create(columns, level)
|
|
18
|
+
columns.filter_map { |column| create_field(column, level) }.size
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def create_field(column, level)
|
|
24
|
+
match = @sensitive.match(column)
|
|
25
|
+
column_level = @muting ? muting_level(level, match) : granting_level(level, match)
|
|
26
|
+
return unless column_level
|
|
27
|
+
|
|
28
|
+
attrs = FieldLevel.flags_for(column_level, muting: @muting)
|
|
29
|
+
@resource.resource_fields.create!(attrs.merge(name: column, label: column.humanize, is_active: true, is_editable: true,
|
|
30
|
+
mask_strategy: match&.strategy || 'full'))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def muting_level(level, match)
|
|
34
|
+
return 'hide' if level == 'hide_sensitive' && match
|
|
35
|
+
return nil if %w[hide_sensitive leave].include?(level) || level.blank?
|
|
36
|
+
|
|
37
|
+
FieldLevel.valid?(level, muting: true) ? level : nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def granting_level(level, match)
|
|
41
|
+
return nil unless FieldLevel.valid?(level, muting: false)
|
|
42
|
+
return 'masked' if match && %w[read edit].include?(level) && @configuration.gdpr_defaults
|
|
43
|
+
|
|
44
|
+
level
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ipaddr'
|
|
4
|
+
|
|
5
|
+
module PermissionForge
|
|
6
|
+
module Admin
|
|
7
|
+
# Activation conditions form (Conditions tab): turns the posted IP list,
|
|
8
|
+
# hour window and session flag into PermissionSet attributes, collecting
|
|
9
|
+
# per-input errors (:ips => [messages], :flag => message).
|
|
10
|
+
class ConditionsForm
|
|
11
|
+
FLAG_FORMAT = /\A[\w.:-]{1,100}\z/
|
|
12
|
+
|
|
13
|
+
attr_reader :errors
|
|
14
|
+
|
|
15
|
+
# @param params [ActionController::Parameters, Hash]
|
|
16
|
+
def initialize(params)
|
|
17
|
+
@params = params
|
|
18
|
+
@errors = {}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Evaluated in order, so errors are keyed :ips before :flag.
|
|
22
|
+
def attributes
|
|
23
|
+
@attributes ||= {
|
|
24
|
+
allowed_ips: ips.join(',').presence,
|
|
25
|
+
allowed_hours_start: hour(:allowed_hours_start),
|
|
26
|
+
allowed_hours_end: hour(:allowed_hours_end),
|
|
27
|
+
requires_session_flag: session_flag
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def ips
|
|
34
|
+
list = @params[:allowed_ips].to_s.split(/[\n,]/).map(&:strip).reject(&:empty?)
|
|
35
|
+
bad = list.each_with_index.reject { |ip, _| valid_ip?(ip) }
|
|
36
|
+
@errors[:ips] = bad.map { |ip, i| I18n.t('permission_forge.errors.admin.ip', line: i + 1, ip: ip) } if bad.any?
|
|
37
|
+
list
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
public
|
|
41
|
+
|
|
42
|
+
# Whether the hour window was switched on in the posted form.
|
|
43
|
+
def hours_enabled?
|
|
44
|
+
@params[:hours_enabled] == '1'
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# The hour posted for `key` as an Integer, or nil when it is not a
|
|
48
|
+
# number (blank, a Hash, an Array) — what the form shows again.
|
|
49
|
+
def submitted_hour(key)
|
|
50
|
+
RecordId.cast(@params[key])
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
# nil unless the hour window is enabled.
|
|
56
|
+
def hour(key)
|
|
57
|
+
submitted_hour(key) if hours_enabled?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def session_flag
|
|
61
|
+
custom = @params[:requires_session_flag_custom].to_s.strip
|
|
62
|
+
flag = custom.presence || @params[:requires_session_flag].to_s.strip
|
|
63
|
+
return flag.presence if flag.blank? || flag.match?(FLAG_FORMAT)
|
|
64
|
+
|
|
65
|
+
@errors[:flag] = I18n.t('permission_forge.errors.admin.flag')
|
|
66
|
+
nil
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# IPAddr::Error is an ArgumentError.
|
|
70
|
+
def valid_ip?(value)
|
|
71
|
+
IPAddr.new(value)
|
|
72
|
+
true
|
|
73
|
+
rescue ArgumentError
|
|
74
|
+
false
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
# The host app's controller actions, resolved the way the runtime guard
|
|
6
|
+
# resolves them (ControllerActions::Mapping) — so the admin lists exactly
|
|
7
|
+
# what the guard checks.
|
|
8
|
+
#
|
|
9
|
+
# Reads the route table once per instance. Routes without a static
|
|
10
|
+
# controller and action (mounted engines and Rack apps, redirects, dynamic
|
|
11
|
+
# segments), internal routes, excluded controllers and actions whose name
|
|
12
|
+
# cannot be a ResourceAction key are skipped. A
|
|
13
|
+
# controller that fails to load, or a host resolver that raises, drops
|
|
14
|
+
# that one entry instead of the whole catalog.
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# catalog = ControllerCatalog.new
|
|
18
|
+
# catalog.keys_for('Customer') # => ['archive', 'export']
|
|
19
|
+
# catalog.for_resource('Customer') # => [#<struct Entry controller_path="customers", action_name="archive", ...>]
|
|
20
|
+
# catalog.any_guarded? # => true once a listed controller runs the guard
|
|
21
|
+
class ControllerCatalog
|
|
22
|
+
# Shown for a route that answers every HTTP verb.
|
|
23
|
+
ANY_VERB = 'ANY'
|
|
24
|
+
FORMAT_SUFFIX = '(.:format)'
|
|
25
|
+
|
|
26
|
+
# One mapped controller action. `key` is the action key the guard asks
|
|
27
|
+
# for; `crud` is :read / :create / :edit / :delete when the action means
|
|
28
|
+
# a CRUD permission, nil for a custom action. `guarded` is whether the
|
|
29
|
+
# controller runs the guard (ControllerActions.guarded?).
|
|
30
|
+
Entry = Struct.new(:controller_path, :action_name, :verbs, :paths, :resource, :key, :crud, :guarded,
|
|
31
|
+
keyword_init: true) do
|
|
32
|
+
def crud? = !crud.nil?
|
|
33
|
+
def guarded? = guarded == true
|
|
34
|
+
|
|
35
|
+
# 'customers#export'
|
|
36
|
+
def endpoint = "#{controller_path}##{action_name}"
|
|
37
|
+
|
|
38
|
+
# A custom (non-CRUD) action of the named resource.
|
|
39
|
+
def custom_for?(resource_name) = resource == resource_name && !crud?
|
|
40
|
+
|
|
41
|
+
def sort_key = [resource, key, controller_path, action_name]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Reads one route: its static controller and action, verbs and path.
|
|
45
|
+
class RoutedAction
|
|
46
|
+
def initialize(route)
|
|
47
|
+
@route = route
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @return [Array(String, String), nil] [controller_path, action_name];
|
|
51
|
+
# nil for an internal route or one without a static controller and
|
|
52
|
+
# action (mounted apps, redirects, dynamic segments)
|
|
53
|
+
def target
|
|
54
|
+
return nil if @route.respond_to?(:internal) && @route.internal
|
|
55
|
+
|
|
56
|
+
parts = @route.defaults.values_at(:controller, :action)
|
|
57
|
+
parts.map(&:to_s) if parts.all? { |part| part.is_a?(String) || part.is_a?(Symbol) }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def verbs
|
|
61
|
+
verbs = @route.verb.to_s.split('|').reject(&:empty?)
|
|
62
|
+
verbs.empty? ? [ANY_VERB] : verbs
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def path
|
|
66
|
+
@route.path.spec.to_s.delete_suffix(FORMAT_SUFFIX)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Adds this route's verbs and path to its target's entry in `found`.
|
|
70
|
+
def merge_into(found)
|
|
71
|
+
merged = (found[target] ||= { verbs: [], paths: [] })
|
|
72
|
+
merged[:verbs] |= verbs
|
|
73
|
+
merged[:paths] |= [path]
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
private_constant :RoutedAction
|
|
77
|
+
|
|
78
|
+
def initialize(configuration: PermissionForge.configuration, routes: Rails.application.routes)
|
|
79
|
+
@configuration = configuration
|
|
80
|
+
@routes = routes
|
|
81
|
+
@controller_classes = {}
|
|
82
|
+
# controller_path => conventional resource, filled by Mapping: one
|
|
83
|
+
# lookup per controller instead of one per route.
|
|
84
|
+
@conventions = {}
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# @return [Array<Entry>] one per mapped (controller_path, action_name),
|
|
88
|
+
# sorted by resource, key, controller and action
|
|
89
|
+
def entries
|
|
90
|
+
@entries ||= routed_actions.filter_map { |(path, action), route| build_entry(path, action, route) }
|
|
91
|
+
.sort_by(&:sort_key).freeze
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# @param resource_name [String, Symbol] e.g. 'Customer'
|
|
95
|
+
# @return [Array<Entry>] the resource's custom (non-CRUD) entries, sorted by key
|
|
96
|
+
def for_resource(resource_name)
|
|
97
|
+
name = resource_name.to_s
|
|
98
|
+
entries.select { |entry| entry.custom_for?(name) }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# @param resource_name [String, Symbol]
|
|
102
|
+
# @return [Array<String>] unique custom action keys, sorted
|
|
103
|
+
def keys_for(resource_name)
|
|
104
|
+
for_resource(resource_name).map(&:key).uniq
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# @return [Boolean] whether any listed controller runs the guard (see
|
|
108
|
+
# ControllerActions.guarded?). When false, permitting or blocking an
|
|
109
|
+
# action changes nothing at runtime.
|
|
110
|
+
def any_guarded?
|
|
111
|
+
entries.any?(&:guarded)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private
|
|
115
|
+
|
|
116
|
+
# { ['customers', 'export'] => { verbs: ['GET'], paths: ['/customers/export'] } }
|
|
117
|
+
def routed_actions
|
|
118
|
+
@routes.routes.each_with_object({}) do |route, found|
|
|
119
|
+
routed = RoutedAction.new(route)
|
|
120
|
+
routed.merge_into(found) if listed?(routed.target)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Has a static target, and its controller is not excluded from discovery.
|
|
125
|
+
def listed?(target)
|
|
126
|
+
target && !@configuration.controller_excluded?(target.first)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def build_entry(path, action, route)
|
|
130
|
+
klass = controller_class(path)
|
|
131
|
+
target = ControllerActions::Mapping.call(controller_path: path, action_name: action, controller_class: klass,
|
|
132
|
+
configuration: @configuration, conventions: @conventions)
|
|
133
|
+
return nil unless target&.manageable?
|
|
134
|
+
|
|
135
|
+
Entry.new(controller_path: path, action_name: action, verbs: route[:verbs].sort, paths: route[:paths].sort,
|
|
136
|
+
resource: target.resource, key: target.action, crud: target.crud,
|
|
137
|
+
guarded: ControllerActions.guarded?(klass))
|
|
138
|
+
rescue StandardError => e
|
|
139
|
+
log_skipped("#{path}##{action}", e)
|
|
140
|
+
nil
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# The controller's class, or nil when it does not exist or cannot be loaded.
|
|
144
|
+
def controller_class(path)
|
|
145
|
+
@controller_classes.fetch(path) { @controller_classes[path] = load_controller(path) }
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# A controller file that fails to load must not take the catalog down.
|
|
149
|
+
def load_controller(path)
|
|
150
|
+
ControllerActions.controller_class_for(path)
|
|
151
|
+
rescue StandardError, ScriptError => e
|
|
152
|
+
log_skipped(path, e)
|
|
153
|
+
nil
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Logged once per catalog: a raising resolver would otherwise repeat the
|
|
157
|
+
# same line for every route.
|
|
158
|
+
def log_skipped(what, error)
|
|
159
|
+
return if @logged
|
|
160
|
+
|
|
161
|
+
@logged = true
|
|
162
|
+
Rails.logger&.warn("[PermissionForge] controller catalog skipped #{what}: #{error.class}: #{error.message}")
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
# @api private
|
|
5
|
+
module Admin
|
|
6
|
+
# The one CSV writer of the admin exports (audit trail, audit log, who
|
|
7
|
+
# holds what): RFC 4180 quoting, CRLF line ends, and a leading apostrophe
|
|
8
|
+
# on any value a spreadsheet would run as a formula (CSV injection).
|
|
9
|
+
module Csv
|
|
10
|
+
# Characters that make Excel, LibreOffice or Google Sheets treat a cell
|
|
11
|
+
# as a formula (or a DDE call) when they open the file — also after
|
|
12
|
+
# leading spaces or line breaks, which some of them skip (and which the
|
|
13
|
+
# audit columns get when a leading tab is cleaned into a space).
|
|
14
|
+
FORMULA_START = /\A(?:[\t\r]|\s*[=+\-@])/
|
|
15
|
+
NEEDS_QUOTES = /[",\r\n]/
|
|
16
|
+
LINE_END = "\r\n"
|
|
17
|
+
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
# @param value [Object] anything; nil is an empty cell
|
|
21
|
+
# @return [String] the cell as written to the file
|
|
22
|
+
def cell(value)
|
|
23
|
+
text = value.to_s
|
|
24
|
+
text = "'#{text}" if text.match?(FORMULA_START)
|
|
25
|
+
text.match?(NEEDS_QUOTES) || text != text.strip ? %("#{text.gsub('"', '""')}") : text
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @param values [Array] the cells of one row
|
|
29
|
+
# @return [String] the row, with its line end
|
|
30
|
+
def line(values)
|
|
31
|
+
values.map { |value| cell(value) }.join(',') << LINE_END
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @param rows [Array<Array>]
|
|
35
|
+
# @return [String]
|
|
36
|
+
def document(rows)
|
|
37
|
+
rows.map { |row| line(row) }.join
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
# The is_editable lock, walked up what a record belongs to: a record is
|
|
6
|
+
# locked when it, its Resource, its Permission Set, or (for a membership)
|
|
7
|
+
# its Permission Set Group has is_editable: false. The admin screens and
|
|
8
|
+
# the JSON API ask this one walk before every change, so a locked
|
|
9
|
+
# Permission Set gains and loses no resources, fields, rules, actions or
|
|
10
|
+
# holders through either surface, and a locked Resource keeps its fields,
|
|
11
|
+
# rules and actions.
|
|
12
|
+
#
|
|
13
|
+
# A set's group is not part of its chain: a locked group freezes its own
|
|
14
|
+
# name, its members and its list of sets (see .locked_group_for_move),
|
|
15
|
+
# not the content of the sets in it — as on the group page.
|
|
16
|
+
#
|
|
17
|
+
# The lock never blocks narrowing access, and always blocks widening it
|
|
18
|
+
# (.widening_lock). Which way a change to the holders goes depends on
|
|
19
|
+
# the set: a granting set gives its holders access, so removing a holder
|
|
20
|
+
# or ending their assignment sooner narrows, and adding a holder,
|
|
21
|
+
# restoring a removal, clearing or extending an end date widens. A
|
|
22
|
+
# Muting Permission Set takes access away from its holders, so each of
|
|
23
|
+
# those changes goes the other way: adding a holder or keeping them
|
|
24
|
+
# longer narrows, removing one or ending their assignment sooner widens.
|
|
25
|
+
#
|
|
26
|
+
# @example
|
|
27
|
+
# EditableLock.locked_record(field) # => the locked Resource or Permission Set, or nil
|
|
28
|
+
# EditableLock.chain(rule) # => [rule, resource, permission_set]
|
|
29
|
+
# EditableLock.widening_lock(set, :revoke) # => the set when it is locked and muting, else nil
|
|
30
|
+
module EditableLock
|
|
31
|
+
# What each record belongs to, for the lock: the first that is present.
|
|
32
|
+
PARENTS = {
|
|
33
|
+
'PermissionForge::Resource' => %i[permission_set],
|
|
34
|
+
'PermissionForge::ResourceField' => %i[resource],
|
|
35
|
+
'PermissionForge::ResourceAction' => %i[resource],
|
|
36
|
+
'PermissionForge::PermissionSetRule' => %i[resource permission_set],
|
|
37
|
+
'PermissionForge::PermissionSetUser' => %i[permission_set],
|
|
38
|
+
'PermissionForge::PermissionSetGroupUser' => %i[permission_set_group]
|
|
39
|
+
}.freeze
|
|
40
|
+
|
|
41
|
+
# What a record is listed in: deleting it changes that list, so a
|
|
42
|
+
# locked container refuses the deletion (a set in a locked group).
|
|
43
|
+
CONTAINERS = { 'PermissionForge::PermissionSet' => :permission_set_group }.freeze
|
|
44
|
+
|
|
45
|
+
module_function
|
|
46
|
+
|
|
47
|
+
# @param record [ActiveRecord::Base, nil]
|
|
48
|
+
# @return [Array<ActiveRecord::Base>] the record, then what it belongs to
|
|
49
|
+
def chain(record)
|
|
50
|
+
records = []
|
|
51
|
+
while record && records.none? { |seen| seen.equal?(record) }
|
|
52
|
+
records << record
|
|
53
|
+
record = parent_of(record)
|
|
54
|
+
end
|
|
55
|
+
records
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @param records [Array<ActiveRecord::Base, nil>]
|
|
59
|
+
# @return [ActiveRecord::Base, nil] the first locked record in their chains
|
|
60
|
+
def locked_record(*records)
|
|
61
|
+
records.flatten.compact.each do |record|
|
|
62
|
+
locked = chain(record).find { |link| locked?(link) }
|
|
63
|
+
return locked if locked
|
|
64
|
+
end
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @return [Boolean] whether the record itself is flagged is_editable: false
|
|
69
|
+
def locked?(record)
|
|
70
|
+
record.respond_to?(:is_editable) && record.is_editable == false
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Moving a set from one Permission Set Group to another changes both
|
|
74
|
+
# groups' lists of sets: refused when either is locked (the group
|
|
75
|
+
# page's add/remove set refuses the same).
|
|
76
|
+
#
|
|
77
|
+
# @param from [Integer, nil] the group id the set has
|
|
78
|
+
# @param to [Integer, String, nil] the group id it would get
|
|
79
|
+
# @return [PermissionForge::PermissionSetGroup, nil] the locked group, if any
|
|
80
|
+
def locked_group_for_move(from:, to:)
|
|
81
|
+
to = RecordId.cast(to)
|
|
82
|
+
return nil if to == from
|
|
83
|
+
|
|
84
|
+
PermissionForge::PermissionSetGroup.where(id: [from, to].compact).detect { |group| locked?(group) }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# The container whose list deleting `record` would change, or nil.
|
|
88
|
+
#
|
|
89
|
+
# @return [ActiveRecord::Base, nil] e.g. a set's Permission Set Group
|
|
90
|
+
def container_of(record)
|
|
91
|
+
association = CONTAINERS[record.class.name]
|
|
92
|
+
association && record.public_send(association)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# How a change moves a person's holding of a set: +1 they hold it more
|
|
96
|
+
# (added, or kept longer), -1 less (removed, or ended sooner), 0 not at
|
|
97
|
+
# all (the same end date again).
|
|
98
|
+
HOLDING = {
|
|
99
|
+
grant: ->(**) { 1 },
|
|
100
|
+
revoke: ->(**) { -1 },
|
|
101
|
+
change_expiry: ->(from:, to:) { expiry_direction(from: from, to: to) }
|
|
102
|
+
}.freeze
|
|
103
|
+
|
|
104
|
+
# Whether a change to the holders of `permission_set` gives anybody
|
|
105
|
+
# more access: holding a granting set more, or a Muting Permission Set
|
|
106
|
+
# less.
|
|
107
|
+
#
|
|
108
|
+
# @param permission_set [PermissionForge::PermissionSet]
|
|
109
|
+
# @param change [Symbol] :grant, :revoke, or :change_expiry (with from:/to:)
|
|
110
|
+
# @param from [Time, nil] the end date the assignment has (:change_expiry)
|
|
111
|
+
# @param to [Time, nil] the one it would get (:change_expiry)
|
|
112
|
+
# @return [Boolean]
|
|
113
|
+
def widens_access?(permission_set, change, **expiry)
|
|
114
|
+
direction = HOLDING.fetch(change).call(**expiry)
|
|
115
|
+
permission_set.is_muting ? direction.negative? : direction.positive?
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# The locked record that refuses a change to the holders of
|
|
119
|
+
# `permission_set`: the set itself when the change widens access
|
|
120
|
+
# (.widens_access?) and the set is locked; nil when the change may go
|
|
121
|
+
# ahead — a narrowing one always may.
|
|
122
|
+
#
|
|
123
|
+
# @return [PermissionForge::PermissionSet, nil]
|
|
124
|
+
def widening_lock(permission_set, change, **expiry)
|
|
125
|
+
return nil unless widens_access?(permission_set, change, **expiry)
|
|
126
|
+
|
|
127
|
+
locked_record(permission_set)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# +1 when the end date moves later or is cleared, -1 when it moves
|
|
131
|
+
# earlier or is set where there was none, 0 when it stays. Compared at
|
|
132
|
+
# the column's precision (microseconds): a preset computes its end with
|
|
133
|
+
# nanoseconds (end_of_day), the stored value has none, and choosing the
|
|
134
|
+
# same preset again is no change.
|
|
135
|
+
#
|
|
136
|
+
# @param from [Time, nil] the end date the assignment has
|
|
137
|
+
# @param to [Time, nil] the one it would get
|
|
138
|
+
# @return [Integer]
|
|
139
|
+
def expiry_direction(from:, to:)
|
|
140
|
+
return 0 if from.nil? && to.nil?
|
|
141
|
+
return 1 if to.nil?
|
|
142
|
+
return -1 if from.nil?
|
|
143
|
+
|
|
144
|
+
to.floor(6) <=> from.floor(6)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def parent_of(record)
|
|
148
|
+
PARENTS.fetch(record.class.name, []).each do |association|
|
|
149
|
+
parent = record.public_send(association)
|
|
150
|
+
return parent if parent
|
|
151
|
+
end
|
|
152
|
+
nil
|
|
153
|
+
end
|
|
154
|
+
private_class_method :parent_of
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|