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,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Api
|
|
5
|
+
class ResourcesController < BaseController
|
|
6
|
+
include CrudActions
|
|
7
|
+
|
|
8
|
+
before_action :set_permission_set
|
|
9
|
+
before_action :set_resource, only: %i[show update destroy generate_fields]
|
|
10
|
+
|
|
11
|
+
# Grants read (and, with grant_all, edit) on every unconfigured column,
|
|
12
|
+
# so it passes the same gates as the admin UI's "Sync fields": the
|
|
13
|
+
# authorization hook, then the lock on the resource and its set.
|
|
14
|
+
def generate_fields
|
|
15
|
+
guard_mutation!(:generate_fields, @resource)
|
|
16
|
+
return if performed?
|
|
17
|
+
|
|
18
|
+
fields = PermissionForge::Services::ResourceFieldGenerator.call(
|
|
19
|
+
resource: @resource,
|
|
20
|
+
grant_all: %w[true 1].include?(params[:grant_all])
|
|
21
|
+
)
|
|
22
|
+
data = PermissionForge::Api::Serializers::ResourceFieldSerializer.collection(fields)
|
|
23
|
+
render json: { data: data }, status: :created
|
|
24
|
+
rescue StandardError => e
|
|
25
|
+
# Log the internal detail; return a generic message so model/column names
|
|
26
|
+
# and adapter errors are not leaked to API clients.
|
|
27
|
+
Rails.logger.error("[PermissionForge] generate_fields failed for #{@resource&.name}: #{e.class}: #{e.message}")
|
|
28
|
+
render json: {
|
|
29
|
+
error: { status: 422, messages: I18n.t('permission_forge.api.errors.field_generation_failed') }
|
|
30
|
+
}, status: :unprocessable_entity
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def collection_scope = @permission_set.resources
|
|
36
|
+
def current_record = @resource
|
|
37
|
+
def serializer_type = :resource
|
|
38
|
+
|
|
39
|
+
def set_permission_set
|
|
40
|
+
@permission_set = find_record(PermissionForge::PermissionSet, :permission_set_identifier)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def set_resource
|
|
44
|
+
@resource = find_record(@permission_set.resources)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def record_params
|
|
48
|
+
pf_require_hash(:resource).permit(
|
|
49
|
+
:name, :description, :is_active, :is_editable,
|
|
50
|
+
:read_access, :create_access, :edit_access, :delete_access,
|
|
51
|
+
:view_all, :modify_all
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
module PermissionForge
|
|
2
|
+
# Base class of the admin screens. Inherits from `config.base_controller_class`, so the host's
|
|
3
|
+
# authentication and layout apply.
|
|
4
|
+
#
|
|
5
|
+
# @api public
|
|
6
|
+
class ApplicationController < PermissionForge.configuration.base_controller_class.constantize
|
|
7
|
+
# Include standard ActionController modules when inheriting from a base
|
|
8
|
+
# that may not have them (e.g. ActionController::Base vs ApplicationController)
|
|
9
|
+
ActionController::Base::MODULES.each do |mod|
|
|
10
|
+
include mod unless self < mod
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
include PermissionForge::ForgeryProtection
|
|
14
|
+
|
|
15
|
+
protect_from_forgery with: :exception, **pf_forgery_strategy
|
|
16
|
+
# Several admin forms submit to more than one action via `formaction`
|
|
17
|
+
# buttons (rule builder refresh, bulk "Check names", "Apply
|
|
18
|
+
# recommendation"). Per-form tokens are bound to the form's own action, so
|
|
19
|
+
# those submissions would fail verification; the session-wide (masked)
|
|
20
|
+
# token still protects every request.
|
|
21
|
+
self.per_form_csrf_tokens = false
|
|
22
|
+
|
|
23
|
+
layout :pf_layout
|
|
24
|
+
|
|
25
|
+
include PermissionForge::BasicAuthentication
|
|
26
|
+
include PermissionForge::TenantRequired
|
|
27
|
+
include PermissionForge::ParamShapes
|
|
28
|
+
include PermissionForge::HtmlFormat
|
|
29
|
+
|
|
30
|
+
THEMES = %w[auto light dark].freeze
|
|
31
|
+
FRAGMENT_HEADER = 'X-PF-Fragment'.freeze
|
|
32
|
+
AUDIT_SOURCE = 'admin_ui'.freeze
|
|
33
|
+
|
|
34
|
+
before_action :set_locale
|
|
35
|
+
before_action :pf_require_tenant
|
|
36
|
+
around_action :pf_without_permission_scope
|
|
37
|
+
# After authentication (a refused sign-in records its own event) and
|
|
38
|
+
# inside the scope suspension (an actor resolver may query).
|
|
39
|
+
around_action :pf_audit_request
|
|
40
|
+
|
|
41
|
+
helper PermissionForge::ApplicationHelper unless self < PermissionForge::ApplicationHelper
|
|
42
|
+
helper_method :pf_fragment?, :pf_theme, :pf_help_visible?, :pf_current_admin,
|
|
43
|
+
:pf_attention, :pf_settings_catalog, :pf_back_or
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
# Memoized per request: drives the Home badge and the home page.
|
|
48
|
+
def pf_attention
|
|
49
|
+
@pf_attention ||= PermissionForge::Admin::AttentionItems.new
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def pf_settings_catalog
|
|
53
|
+
@pf_settings_catalog ||= PermissionForge::Admin::SettingsCatalog.new
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def set_locale
|
|
57
|
+
session[:permission_forge_locale] = params[:locale] if params[:locale].present? && available_locale?(params[:locale])
|
|
58
|
+
|
|
59
|
+
I18n.locale = session[:permission_forge_locale] || I18n.default_locale
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def available_locale?(locale)
|
|
63
|
+
allowed = PermissionForge.configuration.available_locales || PermissionForge::BUNDLED_LOCALES
|
|
64
|
+
Array(allowed).map(&:to_s).include?(locale.to_s)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def default_url_options
|
|
68
|
+
{}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# The admin manages permissions for everyone, so it must see every record:
|
|
72
|
+
# suspend Level 1 auto-scoping / Level 2 detection for the request.
|
|
73
|
+
def pf_without_permission_scope(&)
|
|
74
|
+
PermissionForge.without_permission_scope(&)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Who acts, from where, which request and why — for every audit event of
|
|
78
|
+
# the request; one correlation id for a request that changes something;
|
|
79
|
+
# the read-only pages of Audit::AuditedViews recorded after success.
|
|
80
|
+
# Nothing of it outlives the request (Audit::RequestContext).
|
|
81
|
+
def pf_audit_request(&)
|
|
82
|
+
PermissionForge::Audit::RequestContext.wrap(self, source: AUDIT_SOURCE, &)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Records a refused request (admin.auth_failed, admin.lock_refused …) so
|
|
86
|
+
# that no rollback takes it along. `data` gets method, path and endpoint.
|
|
87
|
+
#
|
|
88
|
+
# pf_audit_refused('admin.auth_failed', data: { 'reason' => 'assignment_not_authorized' }, permission_set: set)
|
|
89
|
+
def pf_audit_refused(event, **payload)
|
|
90
|
+
PermissionForge::Audit::RequestContext.refused(self, event, source: AUDIT_SOURCE, **payload)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Requests from the progressive-enhancement script that load a page into
|
|
94
|
+
# a drawer/dialog get the bare content, without the sidebar layout.
|
|
95
|
+
def pf_fragment?
|
|
96
|
+
request.headers[FRAGMENT_HEADER].present?
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def pf_layout
|
|
100
|
+
pf_fragment? ? 'permission_forge/fragment' : 'permission_forge/application'
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def pf_theme
|
|
104
|
+
theme = cookies[:pf_theme].to_s
|
|
105
|
+
THEMES.include?(theme) ? theme : 'auto'
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def pf_dismissed_help
|
|
109
|
+
cookies[:pf_help].to_s.split(',').grep(/\A[a-z_]+\z/)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def pf_help_visible?(key)
|
|
113
|
+
!pf_dismissed_help.include?(key.to_s)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Cookies are scoped to the engine's mount path so they never collide with
|
|
117
|
+
# (or leak into) the host app's own cookies, and are Secure whenever the
|
|
118
|
+
# admin is served over HTTPS.
|
|
119
|
+
def pf_cookie(name, value)
|
|
120
|
+
cookies[name] = { value: value, path: pf_mount_path, httponly: true, same_site: :lax,
|
|
121
|
+
secure: request.ssl?, expires: 1.year.from_now }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def pf_mount_path
|
|
125
|
+
request.script_name.presence || '/'
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def pf_current_admin
|
|
129
|
+
return @pf_current_admin if defined?(@pf_current_admin)
|
|
130
|
+
|
|
131
|
+
@pf_current_admin = begin
|
|
132
|
+
PermissionForge.current_user
|
|
133
|
+
rescue StandardError
|
|
134
|
+
nil
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Redirect with a toast. `undo:` is { path:, method:, params: } for a
|
|
139
|
+
# one-click reversal rendered inside the toast.
|
|
140
|
+
# Messages: notice:, alert:.
|
|
141
|
+
def pf_redirect(path, undo: nil, **messages)
|
|
142
|
+
flash[:pf_undo] = undo if undo
|
|
143
|
+
redirect_to path, **messages.slice(:notice, :alert)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def pf_back_or(default)
|
|
147
|
+
target = params[:return_to].to_s
|
|
148
|
+
pf_local_path?(target) ? target : default
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Same-origin path ("/x"), never protocol-relative ("//host"), and
|
|
152
|
+
# nothing a browser could read as another host: no backslash ("/\\host")
|
|
153
|
+
# and no control character (a tab or newline inside "//").
|
|
154
|
+
def pf_local_path?(path)
|
|
155
|
+
path.start_with?('/') && !path.start_with?('//') && !path.match?(/[\\[:cntrl:]]/)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Expiry preset + custom date posted by the assign forms.
|
|
159
|
+
def pf_load_expiry
|
|
160
|
+
@expiry = PermissionForge::Admin::ExpiryPresets::KEYS.include?(params[:expiry]) ? params[:expiry] : 'never'
|
|
161
|
+
@custom_date = params[:custom_date].to_s
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Sets @expires_at from the chosen preset; returns the translated error
|
|
165
|
+
# for an invalid custom date, else nil.
|
|
166
|
+
def pf_resolve_expiry
|
|
167
|
+
@expires_at = PermissionForge::Admin::ExpiryPresets.expires_at_for(@expiry, custom: @custom_date)
|
|
168
|
+
nil
|
|
169
|
+
rescue PermissionForge::Admin::ExpiryPresets::InvalidDate => e
|
|
170
|
+
t("permission_forge.errors.admin.expiry_#{e.message}")
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def pf_people_count(permission_set)
|
|
174
|
+
permission_set.permission_set_users.active_assignments.count
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def pf_not_found
|
|
178
|
+
raise ActionController::RoutingError, 'Not Found'
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def pf_locked?(record)
|
|
182
|
+
PermissionForge::Admin::EditableLock.locked?(record)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Redirects with an alert when the record (or any of `records`), or what
|
|
186
|
+
# it belongs to (Admin::EditableLock: its Resource, Permission Set, the
|
|
187
|
+
# Group of a membership), is locked, and records the refusal
|
|
188
|
+
# (admin.lock_refused, naming the locked record); returns whether it was.
|
|
189
|
+
def pf_reject_locked!(records, path, alert: t('permission_forge.flash.not_editable')) # rubocop:disable Naming/PredicateMethod -- redirects; the answer says whether it did
|
|
190
|
+
locked = PermissionForge::Admin::EditableLock.locked_record(records)
|
|
191
|
+
if locked
|
|
192
|
+
pf_audit_refused('admin.lock_refused', record: locked)
|
|
193
|
+
redirect_to path, alert: alert
|
|
194
|
+
end
|
|
195
|
+
locked ? true : false
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
module PermissionForge
|
|
2
|
+
# Serves the admin UI's own stylesheet, script and brand files straight from
|
|
3
|
+
# the gem, so the engine works without Sprockets, Propshaft, importmap or any
|
|
4
|
+
# other host asset pipeline.
|
|
5
|
+
#
|
|
6
|
+
# Inherits from ActionController::Base on purpose — never from the host's
|
|
7
|
+
# base_controller_class — so host before_actions (sign-in redirects, tenant
|
|
8
|
+
# lookups, layouts) can't break or leak into static files. Nothing served
|
|
9
|
+
# here is sensitive: it is the same CSS/JS/logo for every visitor.
|
|
10
|
+
#
|
|
11
|
+
# @api private
|
|
12
|
+
class AssetsController < ActionController::Base
|
|
13
|
+
ROOT = Engine.root.join('app', 'frontend', 'permission_forge').freeze
|
|
14
|
+
|
|
15
|
+
# Allow-list: published name => [relative path, content type].
|
|
16
|
+
FILES = {
|
|
17
|
+
'pf_admin.css' => ['pf_admin.css', 'text/css'],
|
|
18
|
+
'pf_admin.js' => ['pf_admin.js', 'text/javascript'],
|
|
19
|
+
'logo-light.svg' => ['brand/logo-horizontal-light.svg', 'image/svg+xml'],
|
|
20
|
+
'logo-dark.svg' => ['brand/logo-horizontal-dark.svg', 'image/svg+xml'],
|
|
21
|
+
'mark.svg' => ['brand/mark.svg', 'image/svg+xml'],
|
|
22
|
+
'mark-small.svg' => ['brand/mark-small.svg', 'image/svg+xml'],
|
|
23
|
+
'favicon.svg' => ['brand/favicon.svg', 'image/svg+xml'],
|
|
24
|
+
'favicon.ico' => ['brand/favicon.ico', 'image/x-icon'],
|
|
25
|
+
'apple-touch-icon.png' => ['brand/apple-touch-icon.png', 'image/png'],
|
|
26
|
+
'chevron.svg' => ['brand/chevron.svg', 'image/svg+xml']
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
# GET-only; CSRF protection stays on (no-op for GET) so no request path
|
|
30
|
+
# ever runs unprotected. The cross-origin <script> check is skipped: this
|
|
31
|
+
# controller exists to serve pf_admin.js to <script src> tags, and the
|
|
32
|
+
# file is the same public, static script for every visitor.
|
|
33
|
+
include PermissionForge::ForgeryProtection
|
|
34
|
+
|
|
35
|
+
protect_from_forgery with: :exception, **pf_forgery_strategy
|
|
36
|
+
skip_after_action :verify_same_origin_request
|
|
37
|
+
# Rails 8.2 adds Vary: Sec-Fetch-Site to every protected response; the
|
|
38
|
+
# same public file for every visitor needs no per-header cache copies.
|
|
39
|
+
skip_after_action :append_sec_fetch_site_to_vary_header, raise: false
|
|
40
|
+
|
|
41
|
+
def show
|
|
42
|
+
name = params[:file].to_s
|
|
43
|
+
path, type = asset_entry(name)
|
|
44
|
+
return head(:not_found) unless path
|
|
45
|
+
|
|
46
|
+
cache_headers(name)
|
|
47
|
+
return unless stale?(etag: self.class.digest(name), public: true)
|
|
48
|
+
|
|
49
|
+
response.headers['X-Content-Type-Options'] = 'nosniff'
|
|
50
|
+
send_file path, type: type, disposition: 'inline'
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
# [absolute path, content type] for an allow-listed name whose file exists, else nil.
|
|
56
|
+
def asset_entry(name)
|
|
57
|
+
path, type = self.class.entries[name]
|
|
58
|
+
[path, type] if path && File.file?(path)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Fingerprinted URLs (?v=digest) never change content, so they can be
|
|
62
|
+
# cached forever; unversioned requests revalidate via ETag.
|
|
63
|
+
def cache_headers(name)
|
|
64
|
+
version = params[:v]
|
|
65
|
+
if version.present? && version == self.class.digest(name)
|
|
66
|
+
response.headers['Cache-Control'] = 'public, max-age=31536000, immutable'
|
|
67
|
+
else
|
|
68
|
+
expires_in 0, public: true, must_revalidate: true
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
class << self
|
|
73
|
+
# Allow-list resolved to absolute paths: name => [path, content type].
|
|
74
|
+
# Paths come only from FILES, never from the request.
|
|
75
|
+
def entries
|
|
76
|
+
@entries ||= FILES.to_h { |name, (relative, type)| [name, [ROOT.join(relative).to_s, type]] }.freeze
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Short content digest used as the cache-busting `v` query param.
|
|
80
|
+
def digest(name)
|
|
81
|
+
@digests ||= {}
|
|
82
|
+
return @digests[name] if @digests.key?(name) && !reload_assets?
|
|
83
|
+
|
|
84
|
+
entry = FILES[name.to_s]
|
|
85
|
+
@digests[name] = entry ? Digest::SHA256.file(ROOT.join(entry.first)).hexdigest[0, 12] : nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
# Recompute digests on every request in development so CSS/JS edits in
|
|
91
|
+
# the gem show up without a server restart.
|
|
92
|
+
def reload_assets?
|
|
93
|
+
defined?(Rails) && Rails.env.development?
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
module PermissionForge
|
|
2
|
+
# Giving and taking access: PermissionSetUser rows. Every change applies
|
|
3
|
+
# immediately (the model invalidates the person's cached permissions) and
|
|
4
|
+
# goes through the model's callbacks, so each grant, change, review and
|
|
5
|
+
# revocation writes its audit event. config.admin_assignment_authorization
|
|
6
|
+
# is asked first (AssignmentAuthorization).
|
|
7
|
+
#
|
|
8
|
+
# @api private
|
|
9
|
+
class AssignmentsController < ApplicationController
|
|
10
|
+
include PermissionForge::AssignmentAuthorization
|
|
11
|
+
include PermissionForge::ActingAdmin
|
|
12
|
+
|
|
13
|
+
# What config.admin_assignment_authorization is asked for each member action.
|
|
14
|
+
CHANGE_ACTIONS = { update: :change_expiry, destroy: :revoke, keep: :review }.freeze
|
|
15
|
+
|
|
16
|
+
before_action :set_assignment, only: %i[update destroy confirm_destroy keep]
|
|
17
|
+
before_action :change_refused?, only: CHANGE_ACTIONS.keys # redirects when refused
|
|
18
|
+
# Removing a holder of a locked Muting Permission Set gives access back.
|
|
19
|
+
before_action :removal_refused?, only: %i[confirm_destroy destroy] # redirects when refused
|
|
20
|
+
|
|
21
|
+
def new
|
|
22
|
+
load_form
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def create
|
|
26
|
+
load_form
|
|
27
|
+
@errors = validate_form
|
|
28
|
+
return render(:new, status: :unprocessable_entity) if @errors.any?
|
|
29
|
+
|
|
30
|
+
back = pf_back_or(permission_forge.person_path(@person))
|
|
31
|
+
return if pf_assignment_refused?(@chosen_sets, @person.id, :grant, back)
|
|
32
|
+
|
|
33
|
+
created = assign_chosen_sets
|
|
34
|
+
pf_redirect back, notice: assigned_notice(created), undo: assign_undo(created, back)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Change the end date (presets) or extend by 30 days. On a locked
|
|
38
|
+
# Permission Set only a change that narrows access passes
|
|
39
|
+
# (EditableLock.widening_lock): for a granting set an end date set or
|
|
40
|
+
# moved earlier, for a Muting Permission Set one cleared or moved later.
|
|
41
|
+
def update
|
|
42
|
+
expires_at = new_expiry(params[:expiry].to_s)
|
|
43
|
+
return redirect_to(return_path) if expires_at == :invalid
|
|
44
|
+
return if widening_refused?(expires_at)
|
|
45
|
+
|
|
46
|
+
@assignment.update!(expires_at: expires_at)
|
|
47
|
+
redirect_to return_path, notice: end_changed_notice(expires_at)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def confirm_destroy
|
|
51
|
+
@lost = lost_labels
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Removing a holder of a granting set narrows access, also on a locked
|
|
55
|
+
# set; the toast's Undo (which grants again) is offered only where that
|
|
56
|
+
# is allowed (EditableLock.widening_lock).
|
|
57
|
+
def destroy
|
|
58
|
+
name = person_name(@assignment.user)
|
|
59
|
+
set_name = @assignment.permission_set.name
|
|
60
|
+
undo = restore_undo
|
|
61
|
+
@assignment.destroy!
|
|
62
|
+
pf_redirect return_path, notice: t('permission_forge.flash.assignments.removed', name: name, set: set_name), undo: undo
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Access review "Keep": records the reviewer and the date
|
|
66
|
+
# (PermissionSetUser#mark_reviewed!, one assignment.reviewed event);
|
|
67
|
+
# staleness is measured from the last review.
|
|
68
|
+
def keep
|
|
69
|
+
@assignment.mark_reviewed!(reviewer: pf_acting_admin_record)
|
|
70
|
+
redirect_to return_path, notice: t('permission_forge.flash.assignments.kept',
|
|
71
|
+
name: person_name(@assignment.user), set: @assignment.permission_set.name)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Undo for "Remove": recreates the assignment.
|
|
75
|
+
def restore
|
|
76
|
+
expires_at = restored_expiry
|
|
77
|
+
set, user = restore_target
|
|
78
|
+
return if pf_widening_refused?(pf_widening_lock(set, :grant), return_path) || pf_assignment_refused?(set, user.id, :grant, return_path)
|
|
79
|
+
|
|
80
|
+
restore_assignment(set, user, expires_at)
|
|
81
|
+
redirect_to return_path, notice: t('permission_forge.flash.assignments.restored', name: person_name(user), set: set.name)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Undo for "Assign": removes the assignments just created (narrows
|
|
85
|
+
# access, so also from a granting set locked since; a locked Muting
|
|
86
|
+
# Permission Set keeps its holders).
|
|
87
|
+
def undo
|
|
88
|
+
assignments = undo_assignments
|
|
89
|
+
return if undo_refused?(assignments)
|
|
90
|
+
|
|
91
|
+
PermissionForge::Audit.correlate { assignments.each(&:destroy!) }
|
|
92
|
+
redirect_to return_path, notice: t('permission_forge.flash.assignments.undone')
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
# An assignment of somebody config.user_list_resolver leaves out is not
|
|
98
|
+
# found: the admin cannot manage them through an assignment id either.
|
|
99
|
+
def set_assignment
|
|
100
|
+
@assignment = PermissionForge::PermissionSetUser.includes(:permission_set, :user).find(params[:id])
|
|
101
|
+
PermissionForge::Admin::PeopleDirectory.new.find(@assignment.user_id) || pf_not_found
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def restore_undo
|
|
105
|
+
return if pf_widening_lock(@assignment.permission_set, :grant)
|
|
106
|
+
|
|
107
|
+
restore = { 'permission_set_id' => @assignment.permission_set_id, 'user_id' => @assignment.user_id,
|
|
108
|
+
'expires_at' => @assignment.expires_at&.iso8601, 'return_to' => return_path }
|
|
109
|
+
{ path: permission_forge.restore_assignments_path, method: 'post', params: restore }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# A locked set's end dates only move in the narrowing direction.
|
|
113
|
+
def widening_refused?(expires_at)
|
|
114
|
+
locked = pf_widening_lock(@assignment.permission_set, :change_expiry, from: @assignment.expires_at, to: expires_at)
|
|
115
|
+
pf_widening_refused?(locked, return_path)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def removal_refused?
|
|
119
|
+
pf_widening_refused?(pf_widening_lock(@assignment.permission_set, :revoke), return_path)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def change_refused?
|
|
123
|
+
pf_assignment_refused?(@assignment.permission_set, @assignment.user_id, CHANGE_ACTIONS.fetch(action_name.to_sym), return_path)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def restore_target
|
|
127
|
+
[PermissionForge::PermissionSet.find(params[:permission_set_id]),
|
|
128
|
+
PermissionForge::Admin::PeopleDirectory.new.find(params[:user_id]) || pf_not_found]
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def restore_assignment(set, user, expires_at)
|
|
132
|
+
PermissionForge::PermissionSetUser.find_or_create_by!(permission_set: set, user: user) do |assignment|
|
|
133
|
+
assignment.expires_at = expires_at
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def return_path
|
|
138
|
+
pf_back_or(@assignment ? permission_forge.person_path(@assignment.user_id) : permission_forge.people_path)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def person_name(user)
|
|
142
|
+
PermissionForge::Admin::PersonLabel.new(user).name
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# All or nothing: one transaction (and one correlation id) for every chosen set.
|
|
146
|
+
def assign_chosen_sets
|
|
147
|
+
PermissionForge::PermissionSetUser.transaction do
|
|
148
|
+
@chosen_sets.map do |set|
|
|
149
|
+
PermissionForge::PermissionSetUser.create!(permission_set: set, user: @person, expires_at: @expires_at)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Only assignments of people within config.user_list_resolver are
|
|
155
|
+
# undone (an id naming anyone else is not found).
|
|
156
|
+
def undo_assignments
|
|
157
|
+
assignments = PermissionForge::PermissionSetUser.where(id: pf_id_list(:ids)).includes(:permission_set).to_a
|
|
158
|
+
people = PermissionForge::Admin::PeopleDirectory.new.find_all(assignments.map(&:user_id)).map(&:id)
|
|
159
|
+
pf_not_found unless assignments.all? { |assignment| people.include?(assignment.user_id) }
|
|
160
|
+
assignments
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def undo_refused?(assignments)
|
|
164
|
+
assignments.group_by(&:permission_set).any? do |set, rows|
|
|
165
|
+
pf_widening_refused?(pf_widening_lock(set, :revoke), return_path) || pf_assignment_refused?(set, rows.map(&:user_id), :revoke, return_path)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# --- assign form state ------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
def load_form
|
|
172
|
+
load_person
|
|
173
|
+
load_sets
|
|
174
|
+
@people_counts = PermissionForge::PermissionSetUser.active_assignments.group(:permission_set_id).count
|
|
175
|
+
pf_load_expiry
|
|
176
|
+
@chosen_sets = @sets.select { |set| @selected_ids.include?(set.id) && !@existing_ids.include?(set.id) }
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def load_person
|
|
180
|
+
@person = person_chosen? ? PermissionForge::Admin::PeopleDirectory.new.find(params[:user_id]) : nil
|
|
181
|
+
@query = params[:q].to_s.strip
|
|
182
|
+
@people = @person ? nil : PermissionForge::Admin::PeopleDirectory.new.pick(query: @query, limit: picker_limit)
|
|
183
|
+
@existing_ids = @person ? PermissionForge::PermissionSetUser.where(user_id: @person.id).pluck(:permission_set_id) : []
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# A person was picked (and the form is not switching or searching people).
|
|
187
|
+
def person_chosen?
|
|
188
|
+
params[:user_id].present? && params[:change_person].blank? && params[:search].blank?
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def load_sets
|
|
192
|
+
@set_query = pf_text_param(:set_q)
|
|
193
|
+
@preset_set = PermissionForge::PermissionSet.find_by(id: params[:permission_set_id])
|
|
194
|
+
@selected_ids = (pf_id_list(:set_ids) + [@preset_set&.id]).compact.uniq
|
|
195
|
+
@sets = assignable_sets
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def picker_limit
|
|
199
|
+
@person ? 0 : 8
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# The sets a holder can be added to: not those where that widens access
|
|
203
|
+
# on a locked set (a locked Muting Permission Set takes holders).
|
|
204
|
+
def assignable_sets
|
|
205
|
+
scope = PermissionForge::PermissionSet.order(:is_muting, :name)
|
|
206
|
+
unless @set_query.empty?
|
|
207
|
+
needle = "%#{ActiveRecord::Base.sanitize_sql_like(@set_query.downcase)}%"
|
|
208
|
+
scope = scope.where('LOWER(name) LIKE ?', needle).or(scope.where(id: @selected_ids))
|
|
209
|
+
end
|
|
210
|
+
scope.reject { |set| pf_widening_lock(set, :grant) }
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def validate_form
|
|
214
|
+
errors = {}
|
|
215
|
+
errors[:person] = t('permission_forge.errors.admin.person') unless @person
|
|
216
|
+
errors[:sets] = t('permission_forge.errors.admin.sets') if @chosen_sets.empty?
|
|
217
|
+
expiry_error = pf_resolve_expiry
|
|
218
|
+
errors[:expiry] = expiry_error if expiry_error
|
|
219
|
+
errors
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def assign_undo(created, back)
|
|
223
|
+
{ path: permission_forge.undo_assignments_path, method: 'post',
|
|
224
|
+
params: { 'ids' => created.map(&:id), 'return_to' => back } }
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def end_changed_notice(expires_at)
|
|
228
|
+
t('permission_forge.flash.assignments.end_changed', name: person_name(@assignment.user),
|
|
229
|
+
set: @assignment.permission_set.name,
|
|
230
|
+
when: helpers.pf_expiry(expires_at).first)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# A date that cannot be read is a 400: it must never restore the
|
|
234
|
+
# assignment without an end date.
|
|
235
|
+
def restored_expiry = pf_time_param(:expires_at)
|
|
236
|
+
|
|
237
|
+
def assigned_notice(created)
|
|
238
|
+
sets = created.map { |assignment| assignment.permission_set.name }.to_sentence
|
|
239
|
+
return t('permission_forge.flash.assignments.assigned', name: person_name(@person), sets: sets) if @expires_at.nil?
|
|
240
|
+
|
|
241
|
+
t('permission_forge.flash.assignments.assigned_until', name: person_name(@person), sets: sets,
|
|
242
|
+
date: helpers.pf_date(@expires_at, year: false))
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# :invalid for a blank or unknown key (CHANGE_KEYS has no blank entry).
|
|
246
|
+
def new_expiry(key)
|
|
247
|
+
return extended_expiry if key == 'extend30'
|
|
248
|
+
return :invalid unless PermissionForge::Admin::ExpiryPresets::CHANGE_KEYS.include?(key)
|
|
249
|
+
|
|
250
|
+
PermissionForge::Admin::ExpiryPresets.expires_at_for(key)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def extended_expiry
|
|
254
|
+
base = [@assignment.expires_at&.to_date, Time.zone.today].compact.max
|
|
255
|
+
(base + 30).in_time_zone.end_of_day
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# What the person stops seeing when this (granting) assignment is removed.
|
|
259
|
+
def lost_labels
|
|
260
|
+
permission_set = @assignment.permission_set
|
|
261
|
+
return [] if permission_set.is_muting
|
|
262
|
+
|
|
263
|
+
permission_set.resources.select(&:is_active).map { |r| helpers.pf_resource_plural(r.name) }
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|