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,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
# @api private
|
|
5
|
+
module ApiAuthentication
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
included do
|
|
9
|
+
before_action :verify_api_enabled!
|
|
10
|
+
before_action :authenticate_api_request!
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def verify_api_enabled!
|
|
16
|
+
return if PermissionForge.configuration.api_enabled
|
|
17
|
+
|
|
18
|
+
head :not_found
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# A refused request records api.auth_failed: who the host knows (the
|
|
22
|
+
# audit actor resolver, the signed-in user), else unknown.
|
|
23
|
+
def authenticate_api_request!
|
|
24
|
+
auth_check = PermissionForge.configuration.api_auth
|
|
25
|
+
if auth_check.nil?
|
|
26
|
+
audit_api_auth_failure('api_auth_not_configured', 401)
|
|
27
|
+
return head(:unauthorized)
|
|
28
|
+
end
|
|
29
|
+
return if auth_check.call(self)
|
|
30
|
+
|
|
31
|
+
audit_api_auth_failure('api_auth_denied', 403)
|
|
32
|
+
render json: { error: { status: 403, messages: I18n.t('permission_forge.api.errors.forbidden') } }, status: :forbidden
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def audit_api_auth_failure(reason, status)
|
|
36
|
+
PermissionForge::Audit::RequestContext.refused(self, 'api.auth_failed', source: 'api',
|
|
37
|
+
data: { 'reason' => reason, 'status' => status })
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
module PermissionForge
|
|
2
|
+
# Asks `config.admin_assignment_authorization` before a change to who holds
|
|
3
|
+
# a Permission Set or belongs to a Permission Set Group — in the HTML admin
|
|
4
|
+
# and in the API (there after `api_assignment_authorization`, which keeps
|
|
5
|
+
# its own signature). Segregation of duties lives in the lambda, e.g. no
|
|
6
|
+
# self-assignment:
|
|
7
|
+
#
|
|
8
|
+
# config.admin_assignment_authorization = lambda { |controller, target, user_ids, action|
|
|
9
|
+
# admin = PermissionForge.current_user
|
|
10
|
+
# admin&.admin? && !user_ids.include?(admin.id)
|
|
11
|
+
# }
|
|
12
|
+
#
|
|
13
|
+
# The lambda receives (controller, target, user_ids, action):
|
|
14
|
+
# controller — the admin or API controller handling the request
|
|
15
|
+
# target — the PermissionForge::PermissionSet or ::PermissionSetGroup
|
|
16
|
+
# user_ids — Array of the people concerned, cast to the user model's
|
|
17
|
+
# primary-key type (Integers for bigint keys)
|
|
18
|
+
# action — one of ACTIONS
|
|
19
|
+
# A truthy answer allows; nil (no lambda) allows everything. A refused
|
|
20
|
+
# request changes nothing, so it writes no change event; a raising lambda
|
|
21
|
+
# raises (the request fails, nothing changes).
|
|
22
|
+
#
|
|
23
|
+
# @api private
|
|
24
|
+
module AssignmentAuthorization
|
|
25
|
+
# :grant / :revoke a Permission Set, :change_expiry of an assignment,
|
|
26
|
+
# :review (access review "Keep"), :add_member / :remove_member of a group.
|
|
27
|
+
ACTIONS = %i[grant revoke change_expiry review add_member remove_member].freeze
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# @param target [PermissionForge::PermissionSet, PermissionForge::PermissionSetGroup]
|
|
32
|
+
# @param user_ids [Array, Object] the people concerned
|
|
33
|
+
# @param action [Symbol] one of ACTIONS
|
|
34
|
+
# @return [Boolean]
|
|
35
|
+
def pf_assignment_allowed?(target, user_ids, action)
|
|
36
|
+
raise ArgumentError, "unknown assignment action #{action.inspect}" unless ACTIONS.include?(action)
|
|
37
|
+
|
|
38
|
+
hook = PermissionForge.configuration.admin_assignment_authorization
|
|
39
|
+
return true unless hook
|
|
40
|
+
|
|
41
|
+
hook.call(self, target, pf_cast_user_ids(user_ids), action) ? true : false
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# HTML admin: true (after redirecting with an alert) when any of the
|
|
45
|
+
# targets is refused, so the caller returns before changing anything.
|
|
46
|
+
# The refusal is recorded (admin.auth_failed).
|
|
47
|
+
def pf_assignment_refused?(targets, user_ids, action, path)
|
|
48
|
+
refused = Array(targets).find { |target| !pf_assignment_allowed?(target, user_ids, action) }
|
|
49
|
+
return false unless refused
|
|
50
|
+
|
|
51
|
+
pf_audit_assignment_refusal('admin.auth_failed', refused, user_ids, action)
|
|
52
|
+
redirect_to path, alert: t('permission_forge.flash.assignment_not_authorized')
|
|
53
|
+
true
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# API: true (after rendering a 403) when the target is refused. The
|
|
57
|
+
# refusal is recorded (api.auth_failed).
|
|
58
|
+
def pf_assignment_refused_api?(target, user_ids, action)
|
|
59
|
+
return false if pf_assignment_allowed?(target, user_ids, action)
|
|
60
|
+
|
|
61
|
+
pf_audit_assignment_refusal('api.auth_failed', target, user_ids, action)
|
|
62
|
+
render_not_authorized(I18n.t('permission_forge.api.errors.assignment_not_authorized'))
|
|
63
|
+
true
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# The locked set that refuses a change to its holders because the change
|
|
67
|
+
# widens access — adding to a granting set, removing from a Muting one,
|
|
68
|
+
# an end date moved the matching way (Admin::EditableLock.widening_lock);
|
|
69
|
+
# nil when the change may go ahead.
|
|
70
|
+
#
|
|
71
|
+
# @param change [Symbol] :grant, :revoke or :change_expiry (with from:/to:)
|
|
72
|
+
# @return [PermissionForge::PermissionSet, nil]
|
|
73
|
+
def pf_widening_lock(permission_set, change, **expiry)
|
|
74
|
+
PermissionForge::Admin::EditableLock.widening_lock(permission_set, change, **expiry)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# HTML admin: true (after redirecting with the lock's alert and recording
|
|
78
|
+
# admin.lock_refused) when `locked` (a #pf_widening_lock answer) is a set.
|
|
79
|
+
def pf_widening_refused?(locked, path)
|
|
80
|
+
return false unless locked
|
|
81
|
+
|
|
82
|
+
alert = locked.is_muting ? t('permission_forge.flash.locked_widening_muting') : t('permission_forge.flash.locked_widening')
|
|
83
|
+
pf_reject_locked!(locked, path, alert: alert)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# pf_audit_refused: the including base controller's (HTML or API).
|
|
87
|
+
def pf_audit_assignment_refusal(event, target, user_ids, action)
|
|
88
|
+
payload = PermissionForge::Audit::Refusals.assignment(target, user_ids, action: action,
|
|
89
|
+
hook: 'admin_assignment_authorization')
|
|
90
|
+
pf_audit_refused(event, **payload)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def pf_cast_user_ids(user_ids)
|
|
94
|
+
PermissionForge::Admin::PeopleDirectory::Ids.cast(user_ids)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module PermissionForge
|
|
2
|
+
# The HTML admin's gate in front of the audit log: the log page, one
|
|
3
|
+
# event, the exports, and the audit tabs of a person and a Permission Set.
|
|
4
|
+
# Audit::LogAccess decides (config.audit_log_authorization); a refusal is
|
|
5
|
+
# recorded once per request (admin.auth_failed, reason
|
|
6
|
+
# audit_log_not_authorized) and answered with a 403 — a page that says so,
|
|
7
|
+
# or nothing for a file. A refused request is never recorded as viewed or
|
|
8
|
+
# exported (Audit::AuditedViews records 2xx responses only).
|
|
9
|
+
#
|
|
10
|
+
# @api private
|
|
11
|
+
module AuditLogGate
|
|
12
|
+
REFUSAL_REASON = 'audit_log_not_authorized'.freeze
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
# @return [Boolean] whether this request may read the audit log
|
|
17
|
+
def pf_audit_log_allowed?
|
|
18
|
+
return @pf_audit_log_allowed if defined?(@pf_audit_log_allowed)
|
|
19
|
+
|
|
20
|
+
@pf_audit_log_allowed = PermissionForge::Audit::LogAccess.allowed?(self)
|
|
21
|
+
pf_audit_refused('admin.auth_failed', data: { 'reason' => REFUSAL_REASON }) unless @pf_audit_log_allowed
|
|
22
|
+
@pf_audit_log_allowed
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# before_action: the refusal, unless the log may be read.
|
|
26
|
+
def pf_require_audit_log!
|
|
27
|
+
pf_render_audit_refusal unless pf_audit_log_allowed?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The audit tab of a page (people#show, permission_sets#show) and its
|
|
31
|
+
# CSV are the audit log too; the other tabs need no permission.
|
|
32
|
+
def pf_audit_tab_allowed?
|
|
33
|
+
return true unless @tab == 'audit' || request.format.csv?
|
|
34
|
+
|
|
35
|
+
pf_audit_log_allowed?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Refused on a page with tabs: the page renders with the refusal on its
|
|
39
|
+
# audit tab (403); a file is not sent.
|
|
40
|
+
def pf_refuse_audit_tab
|
|
41
|
+
return head(:forbidden) unless request.format.html?
|
|
42
|
+
|
|
43
|
+
@audit_refused = true
|
|
44
|
+
render :show, status: :forbidden
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def pf_render_audit_refusal
|
|
48
|
+
if request.format.html?
|
|
49
|
+
render 'permission_forge/shared/audit_refused', status: :forbidden
|
|
50
|
+
else
|
|
51
|
+
head :forbidden
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
module PermissionForge
|
|
2
|
+
# HTTP Basic authentication for the admin screens, driven by `config.http_basic_auth_*`;
|
|
3
|
+
# the engine's controllers include it.
|
|
4
|
+
#
|
|
5
|
+
# @api public
|
|
6
|
+
module BasicAuthentication
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
# A username is stored as the actor label of an event: a varchar(255).
|
|
10
|
+
AUDIT_USERNAME_LIMIT = 255
|
|
11
|
+
|
|
12
|
+
included do
|
|
13
|
+
before_action :authenticate_by_http_basic
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def authenticate_by_http_basic
|
|
19
|
+
return unless http_basic_auth_enabled?
|
|
20
|
+
|
|
21
|
+
unless http_basic_auth_configured?
|
|
22
|
+
audit_http_basic_failure('basic_auth_not_configured')
|
|
23
|
+
return head(:unauthorized)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
authenticate_or_request_with_http_basic('PermissionForge') do |given_user, given_password|
|
|
27
|
+
verified = http_basic_credentials_match?(given_user, given_password)
|
|
28
|
+
verified ? remember_http_basic_user : audit_http_basic_failure('invalid_credentials')
|
|
29
|
+
verified
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def http_basic_credentials_match?(given_user, given_password)
|
|
34
|
+
ActiveSupport::SecurityUtils.secure_compare(given_user, http_basic_auth_credentials[:user].to_s) &
|
|
35
|
+
ActiveSupport::SecurityUtils.secure_compare(given_password, http_basic_auth_credentials[:password].to_s)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The verified username names the actor of the request's audit events
|
|
39
|
+
# (Audit::ActorResolver) — the configured one, never what was typed.
|
|
40
|
+
def remember_http_basic_user
|
|
41
|
+
request.env[PermissionForge::Audit::ActorResolver::BASIC_AUTH_ENV] = http_basic_auth_credentials[:user].to_s
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Records admin.auth_failed when credentials were sent: the username as
|
|
45
|
+
# typed (capped, valid UTF-8), never the password. A browser's first
|
|
46
|
+
# request, which sends none, is the challenge, not a failure.
|
|
47
|
+
def audit_http_basic_failure(reason)
|
|
48
|
+
return unless ActionController::HttpAuthentication::Basic.has_basic_credentials?(request)
|
|
49
|
+
|
|
50
|
+
username = ActionController::HttpAuthentication::Basic.user_name_and_password(request).first
|
|
51
|
+
PermissionForge::Audit::RequestContext.refused(
|
|
52
|
+
self, 'admin.auth_failed', source: 'admin_ui',
|
|
53
|
+
actor: { type: 'basic_auth', label: audit_username(username) },
|
|
54
|
+
data: { 'reason' => reason }
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def audit_username(username)
|
|
59
|
+
username.to_s.dup.force_encoding(Encoding::UTF_8).scrub.delete("\u0000")[0, AUDIT_USERNAME_LIMIT]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def http_basic_auth_enabled?
|
|
63
|
+
PermissionForge.configuration.http_basic_auth_enabled
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def http_basic_auth_configured?
|
|
67
|
+
http_basic_auth_credentials.values.all?(&:present?)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def http_basic_auth_credentials
|
|
71
|
+
{
|
|
72
|
+
user: PermissionForge.configuration.http_basic_auth_user,
|
|
73
|
+
password: PermissionForge.configuration.http_basic_auth_password
|
|
74
|
+
}
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
# For admin actions that clear somebody's cached permissions: says what the
|
|
5
|
+
# cache store confirmed, instead of announcing success whatever happened.
|
|
6
|
+
#
|
|
7
|
+
# @api private
|
|
8
|
+
module CacheConfirmation
|
|
9
|
+
extend ActiveSupport::Concern
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
# Whether the cache store confirmed every invalidation made in the block.
|
|
14
|
+
# The gem's own failure bookkeeping is asked, not the value of
|
|
15
|
+
# invalidate_permissions: host apps override that method.
|
|
16
|
+
def cache_confirmed?(&)
|
|
17
|
+
PermissionForge::Cache::Failure.none?(&)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# The notice — or, when the cache store did not confirm that the cached
|
|
21
|
+
# permissions are gone, the alert, which says the old ones may still be
|
|
22
|
+
# served. `keys` names both, under permission_forge.flash.people:
|
|
23
|
+
# `{ notice: :erased, alert: :erased_cache_kept }`.
|
|
24
|
+
#
|
|
25
|
+
# @return [Hash] for redirect_to
|
|
26
|
+
def cache_flash(confirmed, keys, **)
|
|
27
|
+
kind = confirmed ? :notice : :alert
|
|
28
|
+
key = keys.fetch(kind)
|
|
29
|
+
{ kind => t("permission_forge.flash.people.#{key}", default: key.to_s.humanize, **) }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module PermissionForge
|
|
2
|
+
# How the admin screens and the admin API verify a mutating request. Rails
|
|
3
|
+
# 8.2 checks the browser's Sec-Fetch-Site header, and `load_defaults 8.2`
|
|
4
|
+
# selects `:header_only`, which lets a plain-HTTP request that carries no
|
|
5
|
+
# such header through without any check — a forged request from a browser
|
|
6
|
+
# that does not send it, or to an app served over HTTP. The engine's
|
|
7
|
+
# controllers use `:header_or_legacy_token` whatever the host chose: the
|
|
8
|
+
# header decides when the browser sends it (same-origin and same-site pass
|
|
9
|
+
# without a token, cross-site is refused), and without it the authenticity
|
|
10
|
+
# token is required, as on every earlier Rails. Rails before 8.2 has no
|
|
11
|
+
# such choice and always checks the token.
|
|
12
|
+
#
|
|
13
|
+
# @api private
|
|
14
|
+
module ForgeryProtection
|
|
15
|
+
extend ActiveSupport::Concern
|
|
16
|
+
|
|
17
|
+
STRATEGY = :header_or_legacy_token
|
|
18
|
+
|
|
19
|
+
class_methods do
|
|
20
|
+
# The engine's verification strategy as protect_from_forgery options,
|
|
21
|
+
# empty where Rails offers no choice. Each controller calls
|
|
22
|
+
# protect_from_forgery itself with its own `with:`, so the call stays
|
|
23
|
+
# visible (and Brakeman sees it).
|
|
24
|
+
def pf_forgery_strategy
|
|
25
|
+
respond_to?(:forgery_protection_verification_strategy) ? { using: STRATEGY } : {}
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module PermissionForge
|
|
2
|
+
# The admin screens answer HTML, plus the exports an action declares
|
|
3
|
+
# (pf_exports). Any other Accept header or extension — a scanner's
|
|
4
|
+
# application/json, text/csv on a form — is answered in HTML: the admin
|
|
5
|
+
# has no other templates, and a failing form must not end in a missing
|
|
6
|
+
# template (a 500). Runs first, before anything renders.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# pf_exports %i[csv json], only: %i[index holdings]
|
|
10
|
+
#
|
|
11
|
+
# @api private
|
|
12
|
+
module HtmlFormat
|
|
13
|
+
extend ActiveSupport::Concern
|
|
14
|
+
|
|
15
|
+
included do
|
|
16
|
+
# action name => the formats it answers besides HTML
|
|
17
|
+
class_attribute :pf_export_formats, instance_writer: false, default: {}.freeze
|
|
18
|
+
prepend_before_action :pf_negotiate_html
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class_methods do
|
|
22
|
+
# @param formats [Array<Symbol>] e.g. %i[csv]
|
|
23
|
+
# @param only [Symbol, Array<Symbol>] the actions that export them
|
|
24
|
+
def pf_exports(formats, only:)
|
|
25
|
+
exports = Array(only).to_h { |action| [action.to_s, formats.map(&:to_s).freeze] }
|
|
26
|
+
self.pf_export_formats = pf_export_formats.merge(exports).freeze
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
# The first acceptable format that is HTML or an export of the action
|
|
33
|
+
# (as respond_to would choose), else HTML.
|
|
34
|
+
def pf_negotiate_html
|
|
35
|
+
exports = pf_export_formats.fetch(action_name, [])
|
|
36
|
+
chosen = request.formats.map { |format| format.symbol.to_s }.find { |name| name == 'html' || exports.include?(name) }
|
|
37
|
+
request.format = (chosen || 'html').to_sym
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
module PermissionForge
|
|
2
|
+
# Request parameters in the shape an action expects. A crafted request
|
|
3
|
+
# that sends a String or an Array where a Hash is expected, or a Hash where
|
|
4
|
+
# an id or a date is, gets a 4xx (or the value is ignored) — never a 500.
|
|
5
|
+
#
|
|
6
|
+
# Every attribute hash the admin and the API write from (pf_require_hash,
|
|
7
|
+
# pf_hash_param) and every typed text (pf_text_param) comes without NUL
|
|
8
|
+
# bytes: PostgreSQL refuses one in text (a 500), the other databases would
|
|
9
|
+
# store it. The one place for both surfaces; `reason` keeps its own
|
|
10
|
+
# cleaning (Audit::ColumnValues).
|
|
11
|
+
#
|
|
12
|
+
# @api private
|
|
13
|
+
module ParamShapes
|
|
14
|
+
# Typed text: see #pf_text_param.
|
|
15
|
+
#
|
|
16
|
+
# @return [String]
|
|
17
|
+
def self.text(value)
|
|
18
|
+
value.is_a?(String) ? value.delete("\0").strip : ''
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# `value` with NUL bytes removed from every String in it, however deep.
|
|
22
|
+
#
|
|
23
|
+
# @return [Object] a new Parameters / Array, or the value itself
|
|
24
|
+
def self.without_nul(value)
|
|
25
|
+
case value
|
|
26
|
+
when String then value.delete("\0")
|
|
27
|
+
when Array then value.map { |item| without_nul(item) }
|
|
28
|
+
when ActionController::Parameters then value.transform_values { |item| without_nul(item) }
|
|
29
|
+
else value
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# params.require(key), which must be a Hash of attributes.
|
|
36
|
+
#
|
|
37
|
+
# @raise [ActionController::ParameterMissing] when it is missing, empty or
|
|
38
|
+
# not a Hash (400; the API answers 422)
|
|
39
|
+
def pf_require_hash(key)
|
|
40
|
+
value = params.require(key)
|
|
41
|
+
raise ActionController::ParameterMissing, key unless value.is_a?(ActionController::Parameters)
|
|
42
|
+
|
|
43
|
+
ParamShapes.without_nul(value)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# params[key] when it is a Hash of attributes (without NUL bytes), else
|
|
47
|
+
# an empty one.
|
|
48
|
+
def pf_hash_param(key)
|
|
49
|
+
value = params[key]
|
|
50
|
+
value.is_a?(ActionController::Parameters) ? ParamShapes.without_nul(value) : ActionController::Parameters.new
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Typed text from params[key]: a String without NUL bytes (they cut a
|
|
54
|
+
# quoted SQL literal short, and PostgreSQL refuses them in text) and
|
|
55
|
+
# without surrounding white space; '' for anything else.
|
|
56
|
+
def pf_text_param(key)
|
|
57
|
+
ParamShapes.text(params[key])
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# params[:tab] when it is one of `tabs`, else nil (a Hash or an Array
|
|
61
|
+
# would reach url_for as unpermitted parameters).
|
|
62
|
+
def pf_tab_param(tabs)
|
|
63
|
+
tab = params[:tab]
|
|
64
|
+
tab if tabs.include?(tab)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# The Integer ids in params[key] (one value or a list); anything that is
|
|
68
|
+
# not digits is dropped.
|
|
69
|
+
def pf_id_list(key)
|
|
70
|
+
Array.wrap(params[key]).filter_map { |value| PermissionForge::Admin::RecordId.cast(value) }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# The time params[key] names, nil when it is blank.
|
|
74
|
+
#
|
|
75
|
+
# @raise [ActionController::BadRequest] for a value that is no time:
|
|
76
|
+
# a date that cannot be read must never become "no end date"
|
|
77
|
+
def pf_time_param(key)
|
|
78
|
+
value = params[key]
|
|
79
|
+
return nil if value.blank?
|
|
80
|
+
|
|
81
|
+
time = Time.zone.parse(value.to_s) if value.is_a?(String)
|
|
82
|
+
time || raise(ActionController::BadRequest, "#{key} is not a time")
|
|
83
|
+
rescue ArgumentError
|
|
84
|
+
raise ActionController::BadRequest, "#{key} is not a time"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module PermissionForge
|
|
2
|
+
# The admin screens and the admin API serve one tenant at a time. With
|
|
3
|
+
# tenancy on (config.tenant_model) a request that reaches them with no
|
|
4
|
+
# current tenant is refused — acts_as_tenant scopes nothing without one, so
|
|
5
|
+
# it would read and change every tenant's Permission Sets, assignments and
|
|
6
|
+
# audit log — unless config.admin_tenantless_access = :allow. The refusal
|
|
7
|
+
# is recorded through the including controller's pf_audit_refused
|
|
8
|
+
# (admin.auth_failed / api.auth_failed, reason tenant_missing) and answered
|
|
9
|
+
# with a 403: JSON from the API, plain text from the admin screens (their
|
|
10
|
+
# layout's navigation and counts would read across tenants).
|
|
11
|
+
#
|
|
12
|
+
# The including controller registers `before_action :pf_require_tenant`
|
|
13
|
+
# after authentication and its locale: an unauthenticated request still
|
|
14
|
+
# gets its 401, the refusal is in the request's language, and a host base
|
|
15
|
+
# controller's own callbacks (which set the tenant) have run.
|
|
16
|
+
#
|
|
17
|
+
# @api public
|
|
18
|
+
module TenantRequired
|
|
19
|
+
REFUSAL_REASON = 'tenant_missing'.freeze
|
|
20
|
+
API_SOURCE = 'api'.freeze
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def pf_require_tenant
|
|
25
|
+
return unless pf_tenant_missing?
|
|
26
|
+
|
|
27
|
+
api = self.class::AUDIT_SOURCE == API_SOURCE
|
|
28
|
+
pf_audit_refused(api ? 'api.auth_failed' : 'admin.auth_failed', data: { 'reason' => REFUSAL_REASON })
|
|
29
|
+
if api
|
|
30
|
+
render json: { error: { status: 403, messages: I18n.t('permission_forge.api.errors.tenant_required') } },
|
|
31
|
+
status: :forbidden
|
|
32
|
+
else
|
|
33
|
+
render plain: I18n.t('permission_forge.errors.tenant_required'), status: :forbidden
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def pf_tenant_missing?
|
|
38
|
+
config = PermissionForge.configuration
|
|
39
|
+
config.tenant_enabled? && !config.admin_tenantless_access_allowed? && PermissionForge.ambient_tenant.nil?
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Api
|
|
5
|
+
# Read-only view of the audit log. Gated by api_enabled and api_auth (the
|
|
6
|
+
# base controller) and then by config.audit_log_authorization
|
|
7
|
+
# (Audit::LogAccess). Rows are tenant-scoped by the model.
|
|
8
|
+
class AuditEventsController < BaseController
|
|
9
|
+
before_action :authorize_audit_log!
|
|
10
|
+
|
|
11
|
+
rescue_from PermissionForge::Audit::EventQuery::Invalid do |error|
|
|
12
|
+
render json: { error: { status: 422, messages: error.errors } }, status: :unprocessable_entity
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def index
|
|
16
|
+
render_collection(PermissionForge::Audit::EventQuery.call(params), serializer_for(:audit_event))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def show
|
|
20
|
+
render_record(find_record(PermissionForge::AuditEvent), serializer_for(:audit_event))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def authorize_audit_log!
|
|
26
|
+
return if PermissionForge::Audit::LogAccess.allowed?(self)
|
|
27
|
+
|
|
28
|
+
pf_audit_refused('api.auth_failed', data: { 'reason' => 'audit_log_not_authorized' })
|
|
29
|
+
render_not_authorized
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|