permission_forge 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +1138 -0
- data/LICENSE.txt +21 -0
- data/README.md +356 -0
- data/app/controllers/concerns/permission_forge/acting_admin.rb +17 -0
- data/app/controllers/concerns/permission_forge/api/crud_actions.rb +84 -0
- data/app/controllers/concerns/permission_forge/api/nested_resource_lookup.rb +18 -0
- data/app/controllers/concerns/permission_forge/api/pagination.rb +50 -0
- data/app/controllers/concerns/permission_forge/api/response_renderer.rb +65 -0
- data/app/controllers/concerns/permission_forge/api/serializer_resolver.rb +30 -0
- data/app/controllers/concerns/permission_forge/api_authentication.rb +40 -0
- data/app/controllers/concerns/permission_forge/assignment_authorization.rb +97 -0
- data/app/controllers/concerns/permission_forge/audit_log_gate.rb +55 -0
- data/app/controllers/concerns/permission_forge/basic_authentication.rb +77 -0
- data/app/controllers/concerns/permission_forge/cache_confirmation.rb +32 -0
- data/app/controllers/concerns/permission_forge/forgery_protection.rb +29 -0
- data/app/controllers/concerns/permission_forge/html_format.rb +40 -0
- data/app/controllers/concerns/permission_forge/param_shapes.rb +87 -0
- data/app/controllers/concerns/permission_forge/tenant_required.rb +42 -0
- data/app/controllers/permission_forge/api/audit_events_controller.rb +33 -0
- data/app/controllers/permission_forge/api/base_controller.rb +220 -0
- data/app/controllers/permission_forge/api/dashboard_controller.rb +30 -0
- data/app/controllers/permission_forge/api/permission_set_group_users_controller.rb +51 -0
- data/app/controllers/permission_forge/api/permission_set_groups_controller.rb +71 -0
- data/app/controllers/permission_forge/api/permission_set_rules_controller.rb +37 -0
- data/app/controllers/permission_forge/api/permission_set_users_controller.rb +105 -0
- data/app/controllers/permission_forge/api/permission_sets_controller.rb +87 -0
- data/app/controllers/permission_forge/api/resource_actions_controller.rb +58 -0
- data/app/controllers/permission_forge/api/resource_fields_controller.rb +30 -0
- data/app/controllers/permission_forge/api/resources_controller.rb +56 -0
- data/app/controllers/permission_forge/application_controller.rb +198 -0
- data/app/controllers/permission_forge/assets_controller.rb +97 -0
- data/app/controllers/permission_forge/assignments_controller.rb +266 -0
- data/app/controllers/permission_forge/audit_events_controller.rb +94 -0
- data/app/controllers/permission_forge/bulk_assignments_controller.rb +108 -0
- data/app/controllers/permission_forge/field_lookups_controller.rb +38 -0
- data/app/controllers/permission_forge/home_controller.rb +24 -0
- data/app/controllers/permission_forge/maintenance_controller.rb +13 -0
- data/app/controllers/permission_forge/people_controller.rb +190 -0
- data/app/controllers/permission_forge/permission_set_editor.rb +71 -0
- data/app/controllers/permission_forge/permission_set_group_users_controller.rb +57 -0
- data/app/controllers/permission_forge/permission_set_groups_controller.rb +111 -0
- data/app/controllers/permission_forge/permission_set_rules_controller.rb +97 -0
- data/app/controllers/permission_forge/permission_sets_controller.rb +227 -0
- data/app/controllers/permission_forge/preferences_controller.rb +26 -0
- data/app/controllers/permission_forge/resource_actions_controller.rb +106 -0
- data/app/controllers/permission_forge/resource_fields_controller.rb +129 -0
- data/app/controllers/permission_forge/resources_controller.rb +189 -0
- data/app/controllers/permission_forge/settings_controller.rb +33 -0
- data/app/frontend/permission_forge/brand/apple-touch-icon.png +0 -0
- data/app/frontend/permission_forge/brand/chevron.svg +1 -0
- data/app/frontend/permission_forge/brand/favicon.ico +0 -0
- data/app/frontend/permission_forge/brand/favicon.svg +1 -0
- data/app/frontend/permission_forge/brand/logo-horizontal-dark.svg +1 -0
- data/app/frontend/permission_forge/brand/logo-horizontal-light.svg +1 -0
- data/app/frontend/permission_forge/brand/mark-small.svg +1 -0
- data/app/frontend/permission_forge/brand/mark.svg +1 -0
- data/app/frontend/permission_forge/pf_admin.css +766 -0
- data/app/frontend/permission_forge/pf_admin.js +482 -0
- data/app/helpers/permission_forge/application_helper.rb +167 -0
- data/app/helpers/permission_forge/audit_helper.rb +230 -0
- data/app/helpers/permission_forge/audit_sentences_helper.rb +84 -0
- data/app/helpers/permission_forge/badges_helper.rb +134 -0
- data/app/helpers/permission_forge/changes_helper.rb +117 -0
- data/app/helpers/permission_forge/people_helper.rb +118 -0
- data/app/helpers/permission_forge/sentences_helper.rb +175 -0
- data/app/helpers/permission_forge/settings_helper.rb +152 -0
- data/app/helpers/permission_forge/ui_helper.rb +184 -0
- data/app/models/permission_forge/audit_event.rb +159 -0
- data/app/models/permission_forge/permission_set.rb +100 -0
- data/app/models/permission_forge/permission_set_group.rb +83 -0
- data/app/models/permission_forge/permission_set_group_user.rb +43 -0
- data/app/models/permission_forge/permission_set_rule.rb +99 -0
- data/app/models/permission_forge/permission_set_user.rb +158 -0
- data/app/models/permission_forge/resource.rb +100 -0
- data/app/models/permission_forge/resource_action.rb +58 -0
- data/app/models/permission_forge/resource_field.rb +69 -0
- data/app/views/layouts/permission_forge/application.html.erb +30 -0
- data/app/views/layouts/permission_forge/fragment.html.erb +2 -0
- data/app/views/permission_forge/assignments/_person_options.html.erb +26 -0
- data/app/views/permission_forge/assignments/confirm_destroy.html.erb +24 -0
- data/app/views/permission_forge/assignments/new.html.erb +99 -0
- data/app/views/permission_forge/audit_events/index.html.erb +161 -0
- data/app/views/permission_forge/audit_events/show.html.erb +146 -0
- data/app/views/permission_forge/bulk_assignments/confirm_revoke.html.erb +21 -0
- data/app/views/permission_forge/bulk_assignments/new.html.erb +69 -0
- data/app/views/permission_forge/field_lookups/show.html.erb +32 -0
- data/app/views/permission_forge/home/_audit_card.html.erb +19 -0
- data/app/views/permission_forge/home/_ending_card.html.erb +32 -0
- data/app/views/permission_forge/home/_expired_card.html.erb +34 -0
- data/app/views/permission_forge/home/_privacy_card.html.erb +67 -0
- data/app/views/permission_forge/home/_rules_card.html.erb +23 -0
- data/app/views/permission_forge/home/_stale_card.html.erb +39 -0
- data/app/views/permission_forge/home/_tidy_card.html.erb +18 -0
- data/app/views/permission_forge/home/index.html.erb +69 -0
- data/app/views/permission_forge/people/_access_tab.html.erb +58 -0
- data/app/views/permission_forge/people/_audit_tab.html.erb +93 -0
- data/app/views/permission_forge/people/_effective_field_source.html.erb +11 -0
- data/app/views/permission_forge/people/_effective_tab.html.erb +118 -0
- data/app/views/permission_forge/people/_effective_why.html.erb +48 -0
- data/app/views/permission_forge/people/_why_item.html.erb +19 -0
- data/app/views/permission_forge/people/confirm_remove_all.html.erb +27 -0
- data/app/views/permission_forge/people/consent_preview.html.erb +39 -0
- data/app/views/permission_forge/people/erasure_preview.html.erb +28 -0
- data/app/views/permission_forge/people/index.html.erb +78 -0
- data/app/views/permission_forge/people/show.html.erb +54 -0
- data/app/views/permission_forge/permission_set_groups/confirm_destroy.html.erb +12 -0
- data/app/views/permission_forge/permission_set_groups/index.html.erb +55 -0
- data/app/views/permission_forge/permission_set_groups/show.html.erb +100 -0
- data/app/views/permission_forge/permission_set_rules/_builder.html.erb +145 -0
- data/app/views/permission_forge/permission_set_rules/_value_input.html.erb +42 -0
- data/app/views/permission_forge/permission_set_rules/confirm_destroy.html.erb +13 -0
- data/app/views/permission_forge/permission_set_rules/edit.html.erb +3 -0
- data/app/views/permission_forge/permission_set_rules/new.html.erb +3 -0
- data/app/views/permission_forge/permission_sets/_assignments_tab.html.erb +74 -0
- data/app/views/permission_forge/permission_sets/_audit_tab.html.erb +80 -0
- data/app/views/permission_forge/permission_sets/_conditions_tab.html.erb +83 -0
- data/app/views/permission_forge/permission_sets/_header.html.erb +36 -0
- data/app/views/permission_forge/permission_sets/_records_summary.html.erb +16 -0
- data/app/views/permission_forge/permission_sets/_resource_summary.html.erb +11 -0
- data/app/views/permission_forge/permission_sets/_resources_tab.html.erb +49 -0
- data/app/views/permission_forge/permission_sets/confirm_destroy.html.erb +23 -0
- data/app/views/permission_forge/permission_sets/confirm_switch_mode.html.erb +15 -0
- data/app/views/permission_forge/permission_sets/index.html.erb +83 -0
- data/app/views/permission_forge/permission_sets/new.html.erb +63 -0
- data/app/views/permission_forge/permission_sets/review.html.erb +26 -0
- data/app/views/permission_forge/permission_sets/show.html.erb +50 -0
- data/app/views/permission_forge/resource_actions/_across_sets.html.erb +24 -0
- data/app/views/permission_forge/resource_actions/_add_select.html.erb +12 -0
- data/app/views/permission_forge/resource_actions/_effect.html.erb +25 -0
- data/app/views/permission_forge/resource_actions/_endpoints.html.erb +16 -0
- data/app/views/permission_forge/resource_actions/_form.html.erb +44 -0
- data/app/views/permission_forge/resource_actions/_managed_note.html.erb +11 -0
- data/app/views/permission_forge/resource_actions/confirm_destroy.html.erb +13 -0
- data/app/views/permission_forge/resource_actions/new.html.erb +31 -0
- data/app/views/permission_forge/resource_actions/show.html.erb +39 -0
- data/app/views/permission_forge/resource_fields/_explain.html.erb +24 -0
- data/app/views/permission_forge/resource_fields/show.html.erb +186 -0
- data/app/views/permission_forge/resources/_actions_tab.html.erb +88 -0
- data/app/views/permission_forge/resources/_fields_tab.html.erb +89 -0
- data/app/views/permission_forge/resources/_rules_tab.html.erb +55 -0
- data/app/views/permission_forge/resources/_sync.html.erb +57 -0
- data/app/views/permission_forge/resources/confirm_destroy.html.erb +17 -0
- data/app/views/permission_forge/resources/new.html.erb +57 -0
- data/app/views/permission_forge/resources/show.html.erb +117 -0
- data/app/views/permission_forge/settings/_ladder.html.erb +15 -0
- data/app/views/permission_forge/settings/_row.html.erb +38 -0
- data/app/views/permission_forge/settings/show.html.erb +67 -0
- data/app/views/permission_forge/shared/_across_sets.html.erb +31 -0
- data/app/views/permission_forge/shared/_audit_events.html.erb +36 -0
- data/app/views/permission_forge/shared/_audit_footer.html.erb +16 -0
- data/app/views/permission_forge/shared/_audit_off_note.html.erb +15 -0
- data/app/views/permission_forge/shared/_audit_pagination.html.erb +19 -0
- data/app/views/permission_forge/shared/_audit_refused_note.html.erb +7 -0
- data/app/views/permission_forge/shared/_audit_timeline.html.erb +29 -0
- data/app/views/permission_forge/shared/_crud_cell.html.erb +18 -0
- data/app/views/permission_forge/shared/_dialog_frame.html.erb +11 -0
- data/app/views/permission_forge/shared/_expiry_picker.html.erb +27 -0
- data/app/views/permission_forge/shared/_head_extra.html.erb +3 -0
- data/app/views/permission_forge/shared/_help_banner.html.erb +10 -0
- data/app/views/permission_forge/shared/_level_control.html.erb +18 -0
- data/app/views/permission_forge/shared/_mode_banners.html.erb +22 -0
- data/app/views/permission_forge/shared/_nav.html.erb +12 -0
- data/app/views/permission_forge/shared/_preferences.html.erb +24 -0
- data/app/views/permission_forge/shared/_reason_field.html.erb +20 -0
- data/app/views/permission_forge/shared/_savebar.html.erb +13 -0
- data/app/views/permission_forge/shared/_sheet_frame.html.erb +22 -0
- data/app/views/permission_forge/shared/_sidebar.html.erb +18 -0
- data/app/views/permission_forge/shared/_tech_details.html.erb +20 -0
- data/app/views/permission_forge/shared/_toasts.html.erb +27 -0
- data/app/views/permission_forge/shared/_topbar.html.erb +15 -0
- data/app/views/permission_forge/shared/_user_select.html.erb +16 -0
- data/app/views/permission_forge/shared/audit_refused.html.erb +7 -0
- data/config/locales/permission_forge.en.yml +2115 -0
- data/config/locales/permission_forge.mk.yml +2166 -0
- data/config/locales/permission_forge.sq.yml +2164 -0
- data/config/routes.rb +143 -0
- data/lib/generators/permission_forge/install/install_generator.rb +105 -0
- data/lib/generators/permission_forge/install/templates/initializer.rb.tt +148 -0
- data/lib/generators/permission_forge/install/templates/migration.rb.tt +198 -0
- data/lib/generators/permission_forge/rls/rls_generator.rb +57 -0
- data/lib/generators/permission_forge/rls/templates/enable_permission_forge_rls.rb.tt +15 -0
- data/lib/generators/permission_forge/upgrade/templates/add_admin_ui_columns.rb.tt +8 -0
- data/lib/generators/permission_forge/upgrade/templates/add_audit_events.rb.tt +98 -0
- data/lib/generators/permission_forge/upgrade/templates/add_resource_fields_unique_index.rb.tt +69 -0
- data/lib/generators/permission_forge/upgrade/upgrade_generator.rb +66 -0
- data/lib/permission_forge/admin/action_across_sets.rb +76 -0
- data/lib/permission_forge/admin/action_rows.rb +156 -0
- data/lib/permission_forge/admin/attention_items.rb +207 -0
- data/lib/permission_forge/admin/audit_event_detail.rb +75 -0
- data/lib/permission_forge/admin/audit_log.rb +193 -0
- data/lib/permission_forge/admin/audit_log_days.rb +41 -0
- data/lib/permission_forge/admin/audit_lookups.rb +105 -0
- data/lib/permission_forge/admin/audit_name_history.rb +85 -0
- data/lib/permission_forge/admin/audit_trail.rb +169 -0
- data/lib/permission_forge/admin/bulk_people_matcher.rb +109 -0
- data/lib/permission_forge/admin/change_set.rb +123 -0
- data/lib/permission_forge/admin/change_set_parts/actions.rb +27 -0
- data/lib/permission_forge/admin/change_set_parts/base.rb +58 -0
- data/lib/permission_forge/admin/change_set_parts/context.rb +50 -0
- data/lib/permission_forge/admin/change_set_parts/field_edit.rb +67 -0
- data/lib/permission_forge/admin/change_set_parts/field_levels.rb +71 -0
- data/lib/permission_forge/admin/change_set_parts/new_actions.rb +91 -0
- data/lib/permission_forge/admin/change_set_parts/new_fields.rb +63 -0
- data/lib/permission_forge/admin/change_set_parts/resource_edit.rb +55 -0
- data/lib/permission_forge/admin/change_set_parts/resource_flags.rb +19 -0
- data/lib/permission_forge/admin/change_set_parts/set_attrs.rb +45 -0
- data/lib/permission_forge/admin/change_set_parts/values.rb +20 -0
- data/lib/permission_forge/admin/column_fields.rb +48 -0
- data/lib/permission_forge/admin/conditions_form.rb +78 -0
- data/lib/permission_forge/admin/controller_catalog.rb +166 -0
- data/lib/permission_forge/admin/csv.rb +41 -0
- data/lib/permission_forge/admin/editable_lock.rb +157 -0
- data/lib/permission_forge/admin/effective_access.rb +158 -0
- data/lib/permission_forge/admin/effective_fields.rb +176 -0
- data/lib/permission_forge/admin/expiry_presets.rb +51 -0
- data/lib/permission_forge/admin/exports/document.rb +89 -0
- data/lib/permission_forge/admin/exports/events.rb +51 -0
- data/lib/permission_forge/admin/exports/holdings.rb +118 -0
- data/lib/permission_forge/admin/exports.rb +37 -0
- data/lib/permission_forge/admin/field_across_sets.rb +51 -0
- data/lib/permission_forge/admin/field_detail.rb +112 -0
- data/lib/permission_forge/admin/field_level.rb +74 -0
- data/lib/permission_forge/admin/field_rows.rb +137 -0
- data/lib/permission_forge/admin/last_changes.rb +94 -0
- data/lib/permission_forge/admin/model_catalog.rb +216 -0
- data/lib/permission_forge/admin/people_candidates.rb +108 -0
- data/lib/permission_forge/admin/people_directory.rb +158 -0
- data/lib/permission_forge/admin/permission_set_factory.rb +48 -0
- data/lib/permission_forge/admin/person_label.rb +95 -0
- data/lib/permission_forge/admin/record_id.rb +19 -0
- data/lib/permission_forge/admin/rule_builder.rb +58 -0
- data/lib/permission_forge/admin/rule_form.rb +175 -0
- data/lib/permission_forge/admin/rule_form_input.rb +74 -0
- data/lib/permission_forge/admin/sensitive_columns.rb +68 -0
- data/lib/permission_forge/admin/settings_catalog.rb +261 -0
- data/lib/permission_forge/admin/settings_values.rb +130 -0
- data/lib/permission_forge/api/serializers/audit_event_serializer.rb +27 -0
- data/lib/permission_forge/api/serializers/base_serializer.rb +36 -0
- data/lib/permission_forge/api/serializers/permission_set_group_serializer.rb +12 -0
- data/lib/permission_forge/api/serializers/permission_set_group_user_serializer.rb +11 -0
- data/lib/permission_forge/api/serializers/permission_set_rule_serializer.rb +25 -0
- data/lib/permission_forge/api/serializers/permission_set_serializer.rb +13 -0
- data/lib/permission_forge/api/serializers/permission_set_user_serializer.rb +11 -0
- data/lib/permission_forge/api/serializers/resource_action_serializer.rb +13 -0
- data/lib/permission_forge/api/serializers/resource_field_serializer.rb +14 -0
- data/lib/permission_forge/api/serializers/resource_serializer.rb +15 -0
- data/lib/permission_forge/audit/actor.rb +144 -0
- data/lib/permission_forge/audit/actor_resolver.rb +103 -0
- data/lib/permission_forge/audit/audited_views.rb +208 -0
- data/lib/permission_forge/audit/boot_warnings.rb +58 -0
- data/lib/permission_forge/audit/column_values.rb +56 -0
- data/lib/permission_forge/audit/context.rb +98 -0
- data/lib/permission_forge/audit/detached.rb +62 -0
- data/lib/permission_forge/audit/event_catalog.rb +110 -0
- data/lib/permission_forge/audit/event_query.rb +192 -0
- data/lib/permission_forge/audit/failure.rb +47 -0
- data/lib/permission_forge/audit/field_access.rb +186 -0
- data/lib/permission_forge/audit/gate.rb +167 -0
- data/lib/permission_forge/audit/job_context.rb +32 -0
- data/lib/permission_forge/audit/log_access.rb +36 -0
- data/lib/permission_forge/audit/recorder.rb +168 -0
- data/lib/permission_forge/audit/refusals.rb +61 -0
- data/lib/permission_forge/audit/request_context.rb +152 -0
- data/lib/permission_forge/audit/runtime.rb +178 -0
- data/lib/permission_forge/audit/runtime_emission.rb +135 -0
- data/lib/permission_forge/audit/settings_observer.rb +70 -0
- data/lib/permission_forge/audit/source.rb +54 -0
- data/lib/permission_forge/audit/subscribers/base.rb +69 -0
- data/lib/permission_forge/audit/subscribers/legacy_hooks.rb +97 -0
- data/lib/permission_forge/audit/subscribers/log.rb +45 -0
- data/lib/permission_forge/audit/subscribers.rb +58 -0
- data/lib/permission_forge/audit.rb +302 -0
- data/lib/permission_forge/boot_checks.rb +184 -0
- data/lib/permission_forge/cache/backlog.rb +142 -0
- data/lib/permission_forge/cache/base.rb +200 -0
- data/lib/permission_forge/cache/bulk_invalidation.rb +131 -0
- data/lib/permission_forge/cache/failure.rb +171 -0
- data/lib/permission_forge/cache/handled_errors.rb +82 -0
- data/lib/permission_forge/cache/holder_invalidation.rb +85 -0
- data/lib/permission_forge/cache/invalidation.rb +180 -0
- data/lib/permission_forge/cache/memory_cache.rb +77 -0
- data/lib/permission_forge/cache/null_cache.rb +40 -0
- data/lib/permission_forge/cache/rails_cache.rb +231 -0
- data/lib/permission_forge/cache/rebuild.rb +149 -0
- data/lib/permission_forge/cache/settlement.rb +63 -0
- data/lib/permission_forge/cache/user_keys.rb +206 -0
- data/lib/permission_forge/cache/variant_removal.rb +55 -0
- data/lib/permission_forge/cache/withheld_notice.rb +60 -0
- data/lib/permission_forge/concerns/audited_collection.rb +34 -0
- data/lib/permission_forge/concerns/auto_permission_scoping.rb +158 -0
- data/lib/permission_forge/concerns/controller_action_guard.rb +106 -0
- data/lib/permission_forge/concerns/field_mask_checker.rb +45 -0
- data/lib/permission_forge/concerns/has_active_scope.rb +12 -0
- data/lib/permission_forge/concerns/has_audit_trail.rb +59 -0
- data/lib/permission_forge/concerns/has_permission_set_users.rb +115 -0
- data/lib/permission_forge/concerns/has_tenant_scoping.rb +86 -0
- data/lib/permission_forge/concerns/has_uuid.rb +24 -0
- data/lib/permission_forge/concerns/invalidates_in_batch.rb +207 -0
- data/lib/permission_forge/concerns/invalidates_managed_actions.rb +80 -0
- data/lib/permission_forge/concerns/masked_attributes/alias_access.rb +280 -0
- data/lib/permission_forge/concerns/masked_attributes/store_access.rb +100 -0
- data/lib/permission_forge/concerns/masked_attributes/validation_reads.rb +153 -0
- data/lib/permission_forge/concerns/masked_attributes/virtual_readers.rb +242 -0
- data/lib/permission_forge/concerns/masked_attributes.rb +576 -0
- data/lib/permission_forge/concerns/permission_cache_accessor.rb +416 -0
- data/lib/permission_forge/concerns/permission_scoped_findable.rb +337 -0
- data/lib/permission_forge/concerns/permissioned_user.rb +78 -0
- data/lib/permission_forge/concerns/permissions_memo.rb +160 -0
- data/lib/permission_forge/concerns/permissions_per_context.rb +288 -0
- data/lib/permission_forge/concerns/permissions_regenerator.rb +43 -0
- data/lib/permission_forge/concerns/records_audit_events.rb +286 -0
- data/lib/permission_forge/concerns/resource_permission_checker.rb +116 -0
- data/lib/permission_forge/concerns/validates_column_lengths.rb +69 -0
- data/lib/permission_forge/concerns/validates_dependency.rb +54 -0
- data/lib/permission_forge/conditional_sets.rb +361 -0
- data/lib/permission_forge/configuration/admin_ui_settings.rb +108 -0
- data/lib/permission_forge/configuration/audit_settings.rb +184 -0
- data/lib/permission_forge/configuration/cache_settings.rb +76 -0
- data/lib/permission_forge/configuration/controller_action_settings.rb +112 -0
- data/lib/permission_forge/configuration/gdpr_settings.rb +70 -0
- data/lib/permission_forge/configuration/masked_attributes_settings.rb +83 -0
- data/lib/permission_forge/configuration/permits_attributes_settings.rb +26 -0
- data/lib/permission_forge/configuration/presets.rb +139 -0
- data/lib/permission_forge/configuration/query_scoping_settings.rb +107 -0
- data/lib/permission_forge/configuration/strategy_values.rb +57 -0
- data/lib/permission_forge/configuration/virtual_attributes_settings.rb +65 -0
- data/lib/permission_forge/configuration.rb +624 -0
- data/lib/permission_forge/controller_actions/authorizer.rb +171 -0
- data/lib/permission_forge/controller_actions/managed_index.rb +213 -0
- data/lib/permission_forge/controller_actions/mapping.rb +167 -0
- data/lib/permission_forge/controller_actions.rb +122 -0
- data/lib/permission_forge/deprecation.rb +86 -0
- data/lib/permission_forge/engine.rb +113 -0
- data/lib/permission_forge/execution_state.rb +318 -0
- data/lib/permission_forge/invalidation_batch.rb +206 -0
- data/lib/permission_forge/masked_models.rb +226 -0
- data/lib/permission_forge/masking_sync.rb +271 -0
- data/lib/permission_forge/model_bindings.rb +90 -0
- data/lib/permission_forge/open_transaction.rb +303 -0
- data/lib/permission_forge/operators/constants.rb +41 -0
- data/lib/permission_forge/operators/registry.rb +300 -0
- data/lib/permission_forge/operators/rule_column.rb +120 -0
- data/lib/permission_forge/operators/validations.rb +111 -0
- data/lib/permission_forge/operators/value_casters/number.rb +68 -0
- data/lib/permission_forge/operators/value_casters/temporal.rb +69 -0
- data/lib/permission_forge/operators/value_casters.rb +80 -0
- data/lib/permission_forge/operators/value_typing.rb +115 -0
- data/lib/permission_forge/query_scoping_warnings.rb +68 -0
- data/lib/permission_forge/rake_audit.rb +65 -0
- data/lib/permission_forge/resource_chain.rb +190 -0
- data/lib/permission_forge/rls.rb +209 -0
- data/lib/permission_forge/services/attribute_access_guard.rb +138 -0
- data/lib/permission_forge/services/audit_event_purger.rb +150 -0
- data/lib/permission_forge/services/bulk_permission_set_assignment.rb +108 -0
- data/lib/permission_forge/services/concerns/across_tenants.rb +69 -0
- data/lib/permission_forge/services/concerns/audited_operation.rb +43 -0
- data/lib/permission_forge/services/concerns/clonable.rb +29 -0
- data/lib/permission_forge/services/consent_withdrawal_handler.rb +135 -0
- data/lib/permission_forge/services/dashboard_stats.rb +43 -0
- data/lib/permission_forge/services/data_subject_erasure.rb +306 -0
- data/lib/permission_forge/services/expired_assignment_cleaner.rb +152 -0
- data/lib/permission_forge/services/extra_field_defaults.rb +71 -0
- data/lib/permission_forge/services/field_permission_merger.rb +83 -0
- data/lib/permission_forge/services/filters_response_attributes.rb +144 -0
- data/lib/permission_forge/services/gdpr_field_auditor/text_report.rb +63 -0
- data/lib/permission_forge/services/gdpr_field_auditor.rb +223 -0
- data/lib/permission_forge/services/host_key_guard.rb +110 -0
- data/lib/permission_forge/services/mask_value.rb +93 -0
- data/lib/permission_forge/services/permission_cache_clearer.rb +174 -0
- data/lib/permission_forge/services/permission_hash_builder.rb +305 -0
- data/lib/permission_forge/services/permission_rule_applier.rb +192 -0
- data/lib/permission_forge/services/permission_set_cloner.rb +89 -0
- data/lib/permission_forge/services/permission_set_condition_evaluator.rb +102 -0
- data/lib/permission_forge/services/permission_set_group_cloner.rb +52 -0
- data/lib/permission_forge/services/permits_attributes/nested_records.rb +106 -0
- data/lib/permission_forge/services/permits_attributes.rb +270 -0
- data/lib/permission_forge/services/resource_field_generator.rb +93 -0
- data/lib/permission_forge/services/resource_with_fields_builder.rb +47 -0
- data/lib/permission_forge/services/rls_operator_sql.rb +131 -0
- data/lib/permission_forge/services/rls_policy_generator.rb +365 -0
- data/lib/permission_forge/services/security_audit/action_check.rb +157 -0
- data/lib/permission_forge/services/security_audit/assignment_check.rb +120 -0
- data/lib/permission_forge/services/security_audit/audit_trail_check.rb +166 -0
- data/lib/permission_forge/services/security_audit/base_check.rb +57 -0
- data/lib/permission_forge/services/security_audit/config_check.rb +212 -0
- data/lib/permission_forge/services/security_audit/integrity_check.rb +105 -0
- data/lib/permission_forge/services/security_audit/personal_data_check.rb +79 -0
- data/lib/permission_forge/services/security_audit/privilege_check.rb +67 -0
- data/lib/permission_forge/services/security_audit/rule_check.rb +141 -0
- data/lib/permission_forge/services/security_audit/text_report.rb +46 -0
- data/lib/permission_forge/services/security_audit/user_model_check.rb +67 -0
- data/lib/permission_forge/services/security_audit.rb +75 -0
- data/lib/permission_forge/services/stale_assignment_auditor.rb +68 -0
- data/lib/permission_forge/services/unscoped_query_detector.rb +132 -0
- data/lib/permission_forge/version.rb +3 -0
- data/lib/permission_forge/virtual_fields/discovery.rb +65 -0
- data/lib/permission_forge/virtual_fields.rb +102 -0
- data/lib/permission_forge.rb +464 -0
- data/lib/tasks/permission_forge.rake +31 -0
- data/lib/tasks/permission_forge_audit_events.rake +28 -0
- data/lib/tasks/permission_forge_cache.rake +26 -0
- data/lib/tasks/permission_forge_expiration.rake +30 -0
- data/lib/tasks/permission_forge_gdpr.rake +69 -0
- data/lib/tasks/permission_forge_rls.rake +38 -0
- metadata +515 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
class Configuration
|
|
5
|
+
# Options that only affect the built-in admin UI (never permission checks).
|
|
6
|
+
#
|
|
7
|
+
# @api public
|
|
8
|
+
module AdminUiSettings
|
|
9
|
+
# Lambda returning the model classes offered in "Add a resource".
|
|
10
|
+
# nil (default) eager-loads app/models and lists every concrete
|
|
11
|
+
# ActiveRecord model with a table, excluding PermissionForge's own.
|
|
12
|
+
# config.admin_model_resolver = -> { [Customer, Invoice, Deal] }
|
|
13
|
+
attr_accessor :admin_model_resolver
|
|
14
|
+
|
|
15
|
+
# Friendly labels for the session flags offered on the Conditions tab.
|
|
16
|
+
# Keys are the flag names your request_context_resolver puts in
|
|
17
|
+
# :session_flags. Flags already used by a Permission Set are always shown.
|
|
18
|
+
# config.admin_session_flags = { 'mfa_verified' => 'Signed in with two-factor' }
|
|
19
|
+
attr_accessor :admin_session_flags
|
|
20
|
+
|
|
21
|
+
# Assignments not reviewed for this many days appear under
|
|
22
|
+
# "Stale assignments" on the admin home page. Default: 90.
|
|
23
|
+
attr_accessor :admin_stale_assignment_days
|
|
24
|
+
|
|
25
|
+
# Name of the host app shown under the logo (e.g. "Acme CRM").
|
|
26
|
+
# nil (default) derives it from the Rails application class.
|
|
27
|
+
attr_accessor :admin_app_name
|
|
28
|
+
|
|
29
|
+
# Where the "Back to <app>" link under the logo points. A String, or a
|
|
30
|
+
# lambda receiving the view context (e.g. ->(view) { view.main_app.dashboard_path }).
|
|
31
|
+
# nil (default) uses the host app's root route, or "/".
|
|
32
|
+
attr_accessor :admin_back_url
|
|
33
|
+
|
|
34
|
+
# Base URL for "Configuration docs" links on the Settings page. The
|
|
35
|
+
# setting key is appended as an anchor.
|
|
36
|
+
attr_accessor :admin_docs_url
|
|
37
|
+
|
|
38
|
+
# What the admin screens and the admin JSON API do with a request that
|
|
39
|
+
# has no current tenant while tenancy is on (tenant_model):
|
|
40
|
+
#
|
|
41
|
+
# :deny — refuse it with a 403, recorded as admin.auth_failed /
|
|
42
|
+
# api.auth_failed, reason tenant_missing (default). With no
|
|
43
|
+
# tenant acts_as_tenant scopes nothing, so the request would
|
|
44
|
+
# read and change every tenant's permissions.
|
|
45
|
+
# :allow — serve it: it reads, edits and deletes existing records of
|
|
46
|
+
# every tenant. Creating a record still needs a tenant (the
|
|
47
|
+
# new row's tenant is required): with none, a create is
|
|
48
|
+
# refused as invalid ("must exist"), never stored tenant-less.
|
|
49
|
+
#
|
|
50
|
+
# A String is taken as its Symbol; any other value raises
|
|
51
|
+
# ConfigurationError (StrategyValues). Set the tenant in the controllers
|
|
52
|
+
# the engine inherits from (base_controller_class, api_base_controller_class)
|
|
53
|
+
# or in middleware.
|
|
54
|
+
attr_accessor :admin_tenantless_access
|
|
55
|
+
|
|
56
|
+
# Sets this group's defaults; called from Configuration#initialize.
|
|
57
|
+
# @return [void]
|
|
58
|
+
# @api private
|
|
59
|
+
def set_admin_ui_defaults
|
|
60
|
+
@admin_model_resolver = nil
|
|
61
|
+
@admin_session_flags = {}
|
|
62
|
+
@admin_stale_assignment_days = 90
|
|
63
|
+
@admin_app_name = nil
|
|
64
|
+
@admin_back_url = nil
|
|
65
|
+
@admin_docs_url = 'https://github.com/fetijashari/permission_forge/blob/main/docs/configuration.md'
|
|
66
|
+
@admin_tenantless_access = :deny
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# @return [Boolean] whether an admin or admin API request may run with
|
|
70
|
+
# no current tenant. Only the exact :allow value allows.
|
|
71
|
+
def admin_tenantless_access_allowed?
|
|
72
|
+
admin_tenantless_access.to_s == 'allow'
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# The admin base-controller settings still at the default
|
|
76
|
+
# ::ActionController::Base while tenancy is on: nothing the engine
|
|
77
|
+
# inherits from sets the tenant there (the API's only counts when the
|
|
78
|
+
# API is enabled).
|
|
79
|
+
#
|
|
80
|
+
# @return [Array<Symbol>] e.g. [:base_controller_class]
|
|
81
|
+
def default_admin_controllers_under_tenancy
|
|
82
|
+
return [] unless tenant_enabled?
|
|
83
|
+
|
|
84
|
+
names = { base_controller_class: base_controller_class }
|
|
85
|
+
names[:api_base_controller_class] = api_base_controller_class if api_enabled
|
|
86
|
+
names.select { |_, name| name.to_s.delete_prefix('::') == 'ActionController::Base' }.keys
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# The boot warning for default_admin_controllers_under_tenancy, or nil.
|
|
90
|
+
#
|
|
91
|
+
# @return [String, nil]
|
|
92
|
+
def tenantless_admin_warning
|
|
93
|
+
names = default_admin_controllers_under_tenancy
|
|
94
|
+
return if names.empty?
|
|
95
|
+
|
|
96
|
+
consequence = if admin_tenantless_access_allowed?
|
|
97
|
+
'admin_tenantless_access is :allow, so a request with no tenant reads and changes the ' \
|
|
98
|
+
'existing records of every tenant.'
|
|
99
|
+
else
|
|
100
|
+
'Every admin request with no tenant is refused (admin_tenantless_access :deny).'
|
|
101
|
+
end
|
|
102
|
+
'[PermissionForge] SECURITY warning: tenant_model is set but ActionController::Base, which sets no ' \
|
|
103
|
+
"tenant, is still the base of #{names.join(' and ')}. #{consequence} Point each at a controller " \
|
|
104
|
+
'that sets the current tenant (or set the tenant in middleware).'
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
class Configuration
|
|
5
|
+
# Options for the audit event log ({PermissionForge::Audit}): what is
|
|
6
|
+
# recorded, who is recorded as the actor, what happens when a record
|
|
7
|
+
# cannot be written, and how long events are kept.
|
|
8
|
+
#
|
|
9
|
+
# The older options stay as they are: `audit_trail_enabled` and
|
|
10
|
+
# `audit_user_class` (created_by / updated_by on the live row),
|
|
11
|
+
# `on_rule_bypass` and `on_field_filtered` (callbacks).
|
|
12
|
+
#
|
|
13
|
+
# @api public
|
|
14
|
+
module AuditSettings
|
|
15
|
+
AUDIT_EVENTS = [:auto, true, false].freeze
|
|
16
|
+
CHANGE_FAILURE_STRATEGIES = %i[raise report log].freeze
|
|
17
|
+
HOOK_FAILURE_STRATEGIES = %i[report log raise].freeze
|
|
18
|
+
|
|
19
|
+
# The groups of runtime (access) events that can be stored, as named in
|
|
20
|
+
# the event catalogue (Audit::EventCatalog):
|
|
21
|
+
# denied — access.denied, action.denied
|
|
22
|
+
# bypass — rule.bypassed, query.unscoped
|
|
23
|
+
# field_read — field.access
|
|
24
|
+
# params_filtered — params.filtered, write.masked_skipped
|
|
25
|
+
# cache — permissions.rebuilt, permissions.invalidated
|
|
26
|
+
RUNTIME_GROUPS = %i[denied bypass field_read params_filtered cache].freeze
|
|
27
|
+
|
|
28
|
+
# Whether audit events are stored in the audit_events table.
|
|
29
|
+
# :auto (default) when the table exists; when it does not, a boot
|
|
30
|
+
# warning is logged, the security audit reports it, and changes
|
|
31
|
+
# still work — an upgrade is never broken by an unrun migration
|
|
32
|
+
# true always (a missing table then fails every change under the
|
|
33
|
+
# default :raise strategy)
|
|
34
|
+
# false never (events are still published as notifications)
|
|
35
|
+
attr_reader :audit_events
|
|
36
|
+
|
|
37
|
+
# Lambda(controller) returning who acts in an admin UI or API request: a
|
|
38
|
+
# user, an Audit::Actor, or a Hash { type:, id:, label: }. nil (default)
|
|
39
|
+
# falls back to current_user_resolver; under HTTP Basic auth the
|
|
40
|
+
# supplied username is recorded with actor_type 'basic_auth'.
|
|
41
|
+
# config.audit_actor_resolver = ->(controller) { controller.current_admin }
|
|
42
|
+
attr_accessor :audit_actor_resolver
|
|
43
|
+
|
|
44
|
+
# Lambda(controller) returning the request context stored with each
|
|
45
|
+
# event: a Hash with :request_id and :ip. nil (default) records the
|
|
46
|
+
# request id and the remote IP.
|
|
47
|
+
attr_accessor :audit_request_context
|
|
48
|
+
|
|
49
|
+
# What happens when a change event cannot be written (the table is
|
|
50
|
+
# unreachable, say) — change events are written inside the transaction
|
|
51
|
+
# of the change:
|
|
52
|
+
# :raise (default) the change rolls back: no change without its record
|
|
53
|
+
# :report the change goes through; the error is logged and handed to
|
|
54
|
+
# Rails.error (handled)
|
|
55
|
+
# :log the change goes through; the error is logged only
|
|
56
|
+
attr_reader :audit_change_failure_strategy
|
|
57
|
+
|
|
58
|
+
# What happens when a notification subscriber, a legacy hook, or the
|
|
59
|
+
# store of a runtime event fails: :report (default), :log or :raise.
|
|
60
|
+
attr_reader :audit_hook_failure_strategy
|
|
61
|
+
|
|
62
|
+
# Which runtime event groups are stored (every runtime event is published
|
|
63
|
+
# regardless). Default: %i[denied]. See RUNTIME_GROUPS.
|
|
64
|
+
attr_reader :audit_runtime_events
|
|
65
|
+
|
|
66
|
+
# When true, every audit event is also written to Rails.logger.
|
|
67
|
+
attr_reader :audit_log_subscriber
|
|
68
|
+
|
|
69
|
+
# How long events are kept: a duration (90.days), seconds, or nil
|
|
70
|
+
# (default) to keep them forever. Only the purge service deletes events,
|
|
71
|
+
# and only ones older than this.
|
|
72
|
+
attr_reader :audit_retention_period
|
|
73
|
+
|
|
74
|
+
# Lambda(controller) returning whether the request may read the audit
|
|
75
|
+
# log (pages, exports, the API endpoint). nil (default): anyone with
|
|
76
|
+
# admin access.
|
|
77
|
+
attr_accessor :audit_log_authorization
|
|
78
|
+
|
|
79
|
+
# Lambda(controller, target, user_ids, action) returning whether the
|
|
80
|
+
# request may change who holds a Permission Set or belongs to a group:
|
|
81
|
+
# target is the PermissionSet or PermissionSetGroup, user_ids the people
|
|
82
|
+
# concerned (cast to the user key type), action one of :grant, :revoke,
|
|
83
|
+
# :change_expiry, :review, :add_member, :remove_member. Asked by the HTML
|
|
84
|
+
# admin and by the API assignment endpoints (after
|
|
85
|
+
# api_assignment_authorization). A refusal changes nothing. nil
|
|
86
|
+
# (default): no extra check. See PermissionForge::AssignmentAuthorization.
|
|
87
|
+
attr_accessor :admin_assignment_authorization
|
|
88
|
+
|
|
89
|
+
# Sets this group's defaults; called from Configuration#initialize.
|
|
90
|
+
# @return [void]
|
|
91
|
+
# @api private
|
|
92
|
+
def set_audit_defaults
|
|
93
|
+
@audit_events = :auto
|
|
94
|
+
@audit_actor_resolver = nil
|
|
95
|
+
@audit_request_context = nil
|
|
96
|
+
@audit_change_failure_strategy = :raise
|
|
97
|
+
@audit_hook_failure_strategy = :report
|
|
98
|
+
@audit_runtime_events = %i[denied].freeze
|
|
99
|
+
@audit_log_subscriber = false
|
|
100
|
+
@audit_retention_period = nil
|
|
101
|
+
@audit_log_authorization = nil
|
|
102
|
+
@admin_assignment_authorization = nil
|
|
103
|
+
# a new configuration may become PermissionForge.configuration
|
|
104
|
+
audit_observation_changed!
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# @raise [PermissionForge::ConfigurationError] for anything but :auto,
|
|
108
|
+
# true or false (a string from ENV would otherwise pass for :auto)
|
|
109
|
+
def audit_events=(value)
|
|
110
|
+
@audit_events = one_of!(:audit_events, value, AUDIT_EVENTS)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def audit_change_failure_strategy=(value)
|
|
114
|
+
@audit_change_failure_strategy = one_of!(:audit_change_failure_strategy, value, CHANGE_FAILURE_STRATEGIES)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def audit_hook_failure_strategy=(value)
|
|
118
|
+
@audit_hook_failure_strategy = one_of!(:audit_hook_failure_strategy, value, HOOK_FAILURE_STRATEGIES)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Symbols or strings; nil means none.
|
|
122
|
+
def audit_runtime_events=(value)
|
|
123
|
+
groups = Array(value).map { |group| group.to_s.to_sym }
|
|
124
|
+
unknown = groups - RUNTIME_GROUPS
|
|
125
|
+
unless unknown.empty?
|
|
126
|
+
raise PermissionForge::ConfigurationError,
|
|
127
|
+
"audit_runtime_events: unknown group(s) #{unknown.map(&:inspect).join(', ')} " \
|
|
128
|
+
"(known: #{RUNTIME_GROUPS.map(&:inspect).join(', ')})"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# frozen: a group pushed in place would pass Audit::Gate by
|
|
132
|
+
@audit_runtime_events = groups.uniq.freeze
|
|
133
|
+
audit_observation_changed!
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def audit_log_subscriber=(value)
|
|
137
|
+
@audit_log_subscriber = value
|
|
138
|
+
audit_observation_changed!
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# @param group [Symbol, nil] a runtime group of the catalogue
|
|
142
|
+
# @return [Boolean] whether runtime events of that group are stored
|
|
143
|
+
def store_runtime_group?(group)
|
|
144
|
+
!group.nil? && audit_runtime_events.include?(group.to_sym)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# The one definition of a valid retention period (the setter and
|
|
148
|
+
# Services::AuditEventPurger both ask it).
|
|
149
|
+
#
|
|
150
|
+
# @param value [ActiveSupport::Duration, Numeric, nil]
|
|
151
|
+
# @return [ActiveSupport::Duration, nil] a Duration stays as given (calendar
|
|
152
|
+
# arithmetic), a number is seconds
|
|
153
|
+
# @raise [PermissionForge::ConfigurationError] for anything else
|
|
154
|
+
def self.retention_period(value)
|
|
155
|
+
return if value.nil?
|
|
156
|
+
|
|
157
|
+
period = value.is_a?(ActiveSupport::Duration) || !value.is_a?(Numeric) ? value : value.seconds
|
|
158
|
+
return period if period.is_a?(ActiveSupport::Duration) && period.to_i.positive?
|
|
159
|
+
|
|
160
|
+
raise PermissionForge::ConfigurationError,
|
|
161
|
+
"audit_retention_period must be a positive duration (90.days), a number of seconds or nil (got #{value.inspect})"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def audit_retention_period=(value)
|
|
165
|
+
AuditSettings.retention_period(value)
|
|
166
|
+
@audit_retention_period = value
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
private
|
|
170
|
+
|
|
171
|
+
# Audit::Gate keeps whether a runtime event is stored or heard.
|
|
172
|
+
def audit_observation_changed!
|
|
173
|
+
PermissionForge::Audit::Gate.changed! if defined?(PermissionForge::Audit::Gate)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def one_of!(option, value, allowed)
|
|
177
|
+
return value if allowed.include?(value)
|
|
178
|
+
|
|
179
|
+
raise PermissionForge::ConfigurationError,
|
|
180
|
+
"#{option} must be one of #{allowed.map(&:inspect).join(', ')} (got #{value.inspect})"
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
class Configuration
|
|
5
|
+
# Options for what happens when the cache store fails
|
|
6
|
+
# ({PermissionForge::Cache::Failure}) and for what is cached from inside a
|
|
7
|
+
# database transaction ({PermissionForge::OpenTransaction}).
|
|
8
|
+
#
|
|
9
|
+
# @api public
|
|
10
|
+
module CacheSettings
|
|
11
|
+
# Optional callback when the cache store fails an invalidation: a stamp
|
|
12
|
+
# or a removal it did not confirm after the retries, an entry that could
|
|
13
|
+
# not be removed, or an error inside an after_commit invalidation. The
|
|
14
|
+
# process that failed makes the invalidation when the store answers
|
|
15
|
+
# again (Cache::Backlog); until then, and in other processes, a cached
|
|
16
|
+
# permission hash may be out of date until cache_ttl.
|
|
17
|
+
#
|
|
18
|
+
# Nothing is raised (a raising after_commit makes Rails skip the
|
|
19
|
+
# callbacks of the remaining records); the failure is logged at error
|
|
20
|
+
# level, published as the `cache_failure.permission_forge` notification,
|
|
21
|
+
# and handed to this hook. Receives (operation:, user_id:, keys:,
|
|
22
|
+
# error:, **details). Errors raised by the hook are logged and swallowed.
|
|
23
|
+
#
|
|
24
|
+
# config.on_cache_failure = ->(operation:, user_id:, **) {
|
|
25
|
+
# Sentry.capture_message("permission cache: #{operation} failed for user #{user_id}")
|
|
26
|
+
# }
|
|
27
|
+
attr_accessor :on_cache_failure
|
|
28
|
+
|
|
29
|
+
# Whether a database transaction is treated as reading from a snapshot —
|
|
30
|
+
# the rows as they were when it began, whatever others have committed
|
|
31
|
+
# since (REPEATABLE READ or stronger). A permission hash built inside
|
|
32
|
+
# such a transaction can be older than an invalidation that has already
|
|
33
|
+
# finished, so it is not cached: the request uses it, and
|
|
34
|
+
# `regenerate_permissions` / `invalidate_permissions` called there take
|
|
35
|
+
# effect in the cache once the transaction has committed.
|
|
36
|
+
#
|
|
37
|
+
# :auto (default) decided from what Rails knows, without asking the
|
|
38
|
+
# server: yes on MySQL (its default isolation) and SQLite; on
|
|
39
|
+
# PostgreSQL only inside a transaction opened with
|
|
40
|
+
# `isolation: :repeatable_read` or `:serializable`. Any other
|
|
41
|
+
# adapter counts as a snapshot.
|
|
42
|
+
# true every transaction — for a PostgreSQL whose default isolation
|
|
43
|
+
# was raised for the database or the role
|
|
44
|
+
# (`default_transaction_isolation`).
|
|
45
|
+
# false none — for a MySQL run at READ COMMITTED. Inside a
|
|
46
|
+
# transaction everything is then cached and invalidated as it
|
|
47
|
+
# is outside one.
|
|
48
|
+
#
|
|
49
|
+
# Where transactions read from a snapshot, permissions first read
|
|
50
|
+
# inside a transaction are built once per transaction and not cached:
|
|
51
|
+
# read them once before a loop of saves.
|
|
52
|
+
attr_reader :snapshot_transactions
|
|
53
|
+
|
|
54
|
+
SNAPSHOT_TRANSACTIONS = [:auto, true, false].freeze
|
|
55
|
+
|
|
56
|
+
# @raise [PermissionForge::ConfigurationError] for anything else (a
|
|
57
|
+
# string from ENV, say), which would otherwise pass for :auto
|
|
58
|
+
def snapshot_transactions=(value)
|
|
59
|
+
unless SNAPSHOT_TRANSACTIONS.include?(value)
|
|
60
|
+
raise PermissionForge::ConfigurationError,
|
|
61
|
+
"snapshot_transactions must be :auto, true or false (got #{value.inspect})"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
@snapshot_transactions = value
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Sets this group's defaults; called from Configuration#initialize.
|
|
68
|
+
# @return [void]
|
|
69
|
+
# @api private
|
|
70
|
+
def set_cache_failure_defaults
|
|
71
|
+
@on_cache_failure = nil
|
|
72
|
+
@snapshot_transactions = :auto
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
class Configuration
|
|
5
|
+
# Options for controller actions: discovery in the admin and the runtime
|
|
6
|
+
# guard ({PermissionForge::Concerns::ControllerActionGuard}).
|
|
7
|
+
#
|
|
8
|
+
# @api public
|
|
9
|
+
module ControllerActionSettings
|
|
10
|
+
# Rails' own controllers never show up in discovery or get guarded.
|
|
11
|
+
DEFAULT_CONTROLLER_DISCOVERY_EXCLUDE = [
|
|
12
|
+
%r{\A(rails|active_storage|action_mailbox|action_cable|action_text|turbo)(/|\z)}
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
# When true, a Resource's Actions tab lists the host app's controller
|
|
16
|
+
# actions that map to that resource. Default: false.
|
|
17
|
+
attr_accessor :controller_action_discovery
|
|
18
|
+
|
|
19
|
+
# Maps a controller action to a Resource and an action key. Receives
|
|
20
|
+
# (controller_path, action_name) — e.g. ('customers/merges', 'create').
|
|
21
|
+
#
|
|
22
|
+
# ['Customer', 'merge'] — this Resource, this action key
|
|
23
|
+
# false — never managed (not listed, not guarded)
|
|
24
|
+
# nil — use the convention (controller name → model)
|
|
25
|
+
#
|
|
26
|
+
# config.controller_action_resolver = lambda do |controller_path, action_name|
|
|
27
|
+
# ['Customer', 'merge'] if controller_path == 'customers/merges' && action_name == 'create'
|
|
28
|
+
# end
|
|
29
|
+
attr_accessor :controller_action_resolver
|
|
30
|
+
|
|
31
|
+
# Controllers left out of discovery and the guard: Regexps, or Strings
|
|
32
|
+
# naming a controller or a namespace ('webhooks' leaves out `webhooks`
|
|
33
|
+
# and `webhooks/*`, not `webhooks_admin`). Blank entries are ignored.
|
|
34
|
+
# PermissionForge's own controllers are always excluded.
|
|
35
|
+
attr_accessor :controller_discovery_exclude
|
|
36
|
+
|
|
37
|
+
# What the guard does with a resource or action no Permission Set manages:
|
|
38
|
+
#
|
|
39
|
+
# :allow — let it through (default — nothing is restricted until an admin
|
|
40
|
+
# adds the action to a Permission Set)
|
|
41
|
+
# :block — deny until a Permission Set permits it
|
|
42
|
+
#
|
|
43
|
+
# A String is taken as its Symbol; any other value raises
|
|
44
|
+
# ConfigurationError (StrategyValues).
|
|
45
|
+
attr_accessor :unconfigured_action_strategy
|
|
46
|
+
|
|
47
|
+
# Lambda(user, controller) returning true to skip the guard for this
|
|
48
|
+
# request (e.g. ->(user, _controller) { user.admin? }). Default: nil.
|
|
49
|
+
attr_accessor :action_guard_bypass
|
|
50
|
+
|
|
51
|
+
# What the guard does with a mapped action when there is no current
|
|
52
|
+
# user, apart from nil_user_strategy (which keeps governing record
|
|
53
|
+
# rules and masking):
|
|
54
|
+
#
|
|
55
|
+
# nil — follow nil_user_strategy (default: no change for existing apps)
|
|
56
|
+
# :bypass — let the request through
|
|
57
|
+
# :deny — refuse it (PermissionForge::ActionNotAuthorizedError)
|
|
58
|
+
#
|
|
59
|
+
# :raise refuses too; nil or blank follows nil_user_strategy; a String
|
|
60
|
+
# is taken as its Symbol; any other value raises ConfigurationError
|
|
61
|
+
# (StrategyValues). New installs get :deny in the
|
|
62
|
+
# generated initializer.
|
|
63
|
+
attr_accessor :action_guard_nil_user_strategy
|
|
64
|
+
|
|
65
|
+
# Sets this group's defaults; called from Configuration#initialize.
|
|
66
|
+
# @return [void]
|
|
67
|
+
# @api private
|
|
68
|
+
def set_controller_action_defaults
|
|
69
|
+
@controller_action_discovery = false
|
|
70
|
+
@controller_action_resolver = nil
|
|
71
|
+
@controller_discovery_exclude = DEFAULT_CONTROLLER_DISCOVERY_EXCLUDE.dup
|
|
72
|
+
@unconfigured_action_strategy = :allow
|
|
73
|
+
@action_guard_bypass = nil
|
|
74
|
+
@action_guard_nil_user_strategy = nil
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# @return [Boolean] whether the guard lets a request with no user
|
|
78
|
+
# through: action_guard_nil_user_strategy, else nil_user_strategy.
|
|
79
|
+
# Only the exact :bypass value lets it through.
|
|
80
|
+
def action_guard_nil_user_bypass?
|
|
81
|
+
(action_guard_nil_user_strategy.presence || nil_user_strategy).to_s == 'bypass'
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# @return [Boolean] whether the admin lists the app's controller actions.
|
|
85
|
+
def controller_action_discovery?
|
|
86
|
+
controller_action_discovery == true
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# @return [Boolean] whether an unmanaged resource or action passes the
|
|
90
|
+
# guard. Only the exact :allow value allows.
|
|
91
|
+
def allow_unconfigured_actions?
|
|
92
|
+
unconfigured_action_strategy.to_s == 'allow'
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# @param controller_path [String] e.g. 'api/v1/customers'
|
|
96
|
+
# @return [Boolean] whether the controller is outside discovery and the guard.
|
|
97
|
+
def controller_excluded?(controller_path)
|
|
98
|
+
path = controller_path.to_s
|
|
99
|
+
return true if path == 'permission_forge' || path.start_with?('permission_forge/')
|
|
100
|
+
|
|
101
|
+
Array(controller_discovery_exclude).any? do |pattern|
|
|
102
|
+
next pattern.match?(path) if pattern.is_a?(Regexp)
|
|
103
|
+
|
|
104
|
+
# A blank String (an unset ENV value) must not exclude everything,
|
|
105
|
+
# and 'custom' must not exclude 'customers'.
|
|
106
|
+
prefix = pattern.to_s.chomp('/')
|
|
107
|
+
prefix.present? && (path == prefix || path.start_with?("#{prefix}/"))
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
class Configuration
|
|
5
|
+
# GDPR-related configuration methods extracted for SRP.
|
|
6
|
+
#
|
|
7
|
+
# @api public
|
|
8
|
+
module GdprSettings
|
|
9
|
+
# Turns the GDPR masking defaults on or off (an explicit setting wins over a preset).
|
|
10
|
+
# @param value [Boolean]
|
|
11
|
+
def gdpr_defaults=(value)
|
|
12
|
+
@gdpr_defaults_explicitly_set = true
|
|
13
|
+
@gdpr_defaults = value
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Replaces the patterns that mark a field name as personal data.
|
|
17
|
+
# @param patterns [Array<Regexp>]
|
|
18
|
+
# @raise [ConfigurationError] for anything that is not a list of regular expressions
|
|
19
|
+
def gdpr_field_patterns=(patterns)
|
|
20
|
+
validate_gdpr_patterns!(patterns)
|
|
21
|
+
@gdpr_field_patterns = patterns
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def gdpr_defaults_explicitly_set?
|
|
25
|
+
@gdpr_defaults_explicitly_set
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Remembers the digest of the current patterns (it tells a changed list from the default).
|
|
29
|
+
# @return [String]
|
|
30
|
+
# @api private
|
|
31
|
+
def record_gdpr_pattern_checksum
|
|
32
|
+
@gdpr_pattern_checksum = compute_pattern_checksum
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def gdpr_patterns_changed_since_generation?
|
|
36
|
+
return false unless @gdpr_pattern_checksum
|
|
37
|
+
|
|
38
|
+
@gdpr_pattern_checksum != compute_pattern_checksum
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def validate_gdpr_explicit_choice!
|
|
42
|
+
return unless @gdpr_require_explicit_choice
|
|
43
|
+
return if gdpr_defaults_explicitly_set?
|
|
44
|
+
|
|
45
|
+
raise PermissionForge::ConfigurationError,
|
|
46
|
+
'gdpr_require_explicit_choice is enabled but gdpr_defaults was not explicitly set. ' \
|
|
47
|
+
'Please set config.gdpr_defaults = true or config.gdpr_defaults = false in your initializer.'
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def validate_gdpr_patterns!(patterns)
|
|
53
|
+
return if patterns.blank?
|
|
54
|
+
|
|
55
|
+
patterns.each_key do |key|
|
|
56
|
+
next if key.is_a?(Regexp)
|
|
57
|
+
|
|
58
|
+
Regexp.new(key)
|
|
59
|
+
rescue RegexpError => e
|
|
60
|
+
raise PermissionForge::ConfigurationError,
|
|
61
|
+
"Invalid GDPR field pattern for '#{key}': #{e.message}"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def compute_pattern_checksum
|
|
66
|
+
Digest::SHA256.hexdigest(@gdpr_field_patterns.to_a.map { |k, v| "#{k.source}:#{v}" }.sort.join('|'))
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
class Configuration
|
|
5
|
+
# Configuration for {PermissionForge::Concerns::MaskedAttributes} model-level enforcement.
|
|
6
|
+
#
|
|
7
|
+
# @api public
|
|
8
|
+
module MaskedAttributesSettings
|
|
9
|
+
# Value returned by MaskedAttributes when read_access is denied. Default: nil.
|
|
10
|
+
attr_accessor :hidden_attribute_value
|
|
11
|
+
|
|
12
|
+
# When true (default), MaskedAttributes omits hidden keys from `attributes` /
|
|
13
|
+
# `as_json` / `serializable_hash`. When false, keys are present with hidden_attribute_value.
|
|
14
|
+
attr_accessor :omit_hidden_in_attributes_hash
|
|
15
|
+
|
|
16
|
+
# Controls auto-applying MaskedAttributes to host models.
|
|
17
|
+
#
|
|
18
|
+
# :auto / true — apply to every model that has a Resource record (default)
|
|
19
|
+
# ['Customer', 'Order'] — only apply to these named models
|
|
20
|
+
# false / nil — no auto-apply; dev includes the concern manually
|
|
21
|
+
#
|
|
22
|
+
# Auto-apply runs at engine boot (scanning existing Resource rows) and again
|
|
23
|
+
# whenever a new Resource is created at runtime. Both paths respect this setting.
|
|
24
|
+
attr_accessor :auto_apply_masked_attributes
|
|
25
|
+
|
|
26
|
+
# Protects against persisting a masked/hidden value back to the database.
|
|
27
|
+
# MaskedAttributes overrides column getters, so a host callback like
|
|
28
|
+
# `before_save { self.email = email.strip }` would otherwise read the MASKED
|
|
29
|
+
# value and write it back, corrupting the stored data. This guard remembers
|
|
30
|
+
# the exact sentinel handed out for each attribute and, when that same
|
|
31
|
+
# sentinel is written back, prevents the overwrite.
|
|
32
|
+
#
|
|
33
|
+
# :skip — silently keep the original stored value (default, safe)
|
|
34
|
+
# :raise — raise MaskedAttributes::MaskedWriteError (surfaces the bug)
|
|
35
|
+
# :off — disable the guard (legacy behavior; risks silent corruption)
|
|
36
|
+
attr_accessor :masked_write_protection
|
|
37
|
+
|
|
38
|
+
# What the attribute guard does when there is no current user (a job, a
|
|
39
|
+
# mailer, the console, a resolver that returns nil). Nothing else
|
|
40
|
+
# governs masking without a user: nil_user_strategy is about rows,
|
|
41
|
+
# response_filter_nil_user_strategy about FiltersResponseAttributes.
|
|
42
|
+
#
|
|
43
|
+
# :bypass — every getter returns the stored value (default)
|
|
44
|
+
# :hide — every guarded attribute reads as hidden_attribute_value:
|
|
45
|
+
# getters, read_attribute, `[]`, attributes, as_json and
|
|
46
|
+
# the wrapped virtual readers and store accessors. The
|
|
47
|
+
# primary key and the raw paths (read_attribute_before_type_cast,
|
|
48
|
+
# pluck) are untouched, as for a signed-in user. Code that
|
|
49
|
+
# reads masked models with no user then sees hidden values:
|
|
50
|
+
# set the current user in those jobs and mailers. The user
|
|
51
|
+
# model (user_class and its subclasses) is exempt: sign-in
|
|
52
|
+
# reads it before anyone is signed in (encrypted_password,
|
|
53
|
+
# confirmation_token, lockable counters).
|
|
54
|
+
attr_accessor :masked_attributes_nil_user_strategy
|
|
55
|
+
|
|
56
|
+
# @return [Boolean] whether the strategy is :hide (whatever the model)
|
|
57
|
+
def hide_masked_attributes_without_user?
|
|
58
|
+
@masked_attributes_nil_user_strategy.to_s == 'hide'
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# The one decision of whether a record of klass hides its guarded
|
|
62
|
+
# attributes when nobody is signed in: under :hide, every model but the
|
|
63
|
+
# user model.
|
|
64
|
+
#
|
|
65
|
+
# @param klass [Class]
|
|
66
|
+
# @return [Boolean]
|
|
67
|
+
def hide_masked_attributes_without_user_for?(klass)
|
|
68
|
+
hide_masked_attributes_without_user? && !user_model_class?(klass)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Sets this group's defaults; called from Configuration#initialize.
|
|
72
|
+
# @return [void]
|
|
73
|
+
# @api private
|
|
74
|
+
def set_masked_attributes_defaults
|
|
75
|
+
@hidden_attribute_value = nil
|
|
76
|
+
@omit_hidden_in_attributes_hash = true
|
|
77
|
+
@auto_apply_masked_attributes = :auto
|
|
78
|
+
@masked_write_protection = :skip
|
|
79
|
+
@masked_attributes_nil_user_strategy = :bypass
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
class Configuration
|
|
5
|
+
# Options for the write path: {PermissionForge::Services::PermitsAttributes},
|
|
6
|
+
# which filters incoming params by field-level edit access.
|
|
7
|
+
#
|
|
8
|
+
# @api public
|
|
9
|
+
module PermitsAttributesSettings
|
|
10
|
+
# How PermitsAttributes treats a `<assoc>_attributes` payload when the
|
|
11
|
+
# user's permission hash has no entry for the association's model:
|
|
12
|
+
# false — the payload is dropped (default, fail closed)
|
|
13
|
+
# true — it is filtered against the PARENT resource's fields, as
|
|
14
|
+
# versions before 0.14 did (a key editable on the parent then
|
|
15
|
+
# admits the same key on a child the user was never granted)
|
|
16
|
+
attr_accessor :nested_attributes_parent_fallback
|
|
17
|
+
|
|
18
|
+
# Sets this group's defaults; called from Configuration#initialize.
|
|
19
|
+
# @return [void]
|
|
20
|
+
# @api private
|
|
21
|
+
def set_permits_attributes_defaults
|
|
22
|
+
@nested_attributes_parent_fallback = false
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|