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,288 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Concerns
|
|
5
|
+
# Which permission hash the request context selects for a user with
|
|
6
|
+
# conditional permission sets (PermissionForge::ConditionalSets): the
|
|
7
|
+
# base entry, or the variant of the conditional sets that pass — on a
|
|
8
|
+
# read (PermissionCacheAccessor#permissions_hash), on a rebuild
|
|
9
|
+
# (regenerate_permissions) — and how each entry, base or variant, is
|
|
10
|
+
# built. (What an invalidation removes is Cache::UserKeys; the variants
|
|
11
|
+
# go on erasure and cache:clear, Cache::VariantRemoval.) A user without
|
|
12
|
+
# conditional sets costs it nothing: one nil check.
|
|
13
|
+
#
|
|
14
|
+
# @api private
|
|
15
|
+
module PermissionsPerContext
|
|
16
|
+
# What a read produced: the hash, what selected it (nil without
|
|
17
|
+
# conditional sets), the open transaction it was built in (nil when it
|
|
18
|
+
# was read from the store, or built outside one), and how it came
|
|
19
|
+
# about — nil: read from the store; :base — regenerate_permissions
|
|
20
|
+
# built it, which bumped the version; :variant — a variant built
|
|
21
|
+
# read-through for a base entry read from the store, which did not;
|
|
22
|
+
# :reentrant — asked by the build itself, {} for that call alone. And
|
|
23
|
+
# when the soonest of the user's assignments that went into it ends
|
|
24
|
+
# (epoch seconds, nil for none: EXPIRY_KEY).
|
|
25
|
+
#
|
|
26
|
+
# @api private
|
|
27
|
+
Selected = Data.define(:permissions, :served, :transaction, :rebuilt, :expires_at) do
|
|
28
|
+
def initialize(expires_at: nil, **)
|
|
29
|
+
super
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# What a memo holds, as a base rebuilt by regenerate_permissions.
|
|
33
|
+
def self.of_memo(memo)
|
|
34
|
+
new(permissions: memo.permissions, served: memo.served, transaction: nil, rebuilt: :base, expires_at: memo.expires_at)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @return [PermissionsMemo::Memo] the memo of this hash, good for `version`
|
|
38
|
+
def memo(key, version)
|
|
39
|
+
PermissionsMemo::Memo.new(key: key, version: version, transaction: transaction, permissions: permissions, served: served,
|
|
40
|
+
expires_at: expires_at)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
REENTRANT = Selected.new(permissions: {}.freeze, served: nil, transaction: nil, rebuilt: :reentrant)
|
|
45
|
+
|
|
46
|
+
# The fiber-local ExecutionState slot of the builds (and request-context
|
|
47
|
+
# resolutions) running on this fiber, by permission cache key.
|
|
48
|
+
BUILDING_KEY = :pf_building_permission_keys
|
|
49
|
+
|
|
50
|
+
# Where the stored base entry of a user with an assignment that ends
|
|
51
|
+
# (PermissionSetUser#expires_at) carries when the soonest one ends, in
|
|
52
|
+
# epoch seconds — as it carries the conditional sets: taken off on
|
|
53
|
+
# every read, never part of the hash. The memo of the hash is retired
|
|
54
|
+
# then (PermissionsMemo::Memo#serves?): an instance kept across
|
|
55
|
+
# requests stops granting what the assignment granted when it ends,
|
|
56
|
+
# as the store entry (whose TTL is clamped to it) does.
|
|
57
|
+
EXPIRY_KEY = :assignments_expire_at
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
# The memoized hash, while the request context still selects what it
|
|
62
|
+
# was built for. For a user without conditional sets: always. With
|
|
63
|
+
# them: when the context is the one that selected it (its signature,
|
|
64
|
+
# cheap), or another one that selects the same variant — remembered
|
|
65
|
+
# as such, so the next call is cheap again. nil when the context now
|
|
66
|
+
# selects another variant: the base entry is read again.
|
|
67
|
+
#
|
|
68
|
+
# @return [Hash, nil]
|
|
69
|
+
def pf_memoized_permissions_for_context(key, memo)
|
|
70
|
+
again = memo.for_context { pf_resolve_request_context(key) }
|
|
71
|
+
return unless again
|
|
72
|
+
|
|
73
|
+
pf_memoize_permissions(again) unless again.equal?(memo)
|
|
74
|
+
again.permissions
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# What a base entry read from the store serves in the current request
|
|
78
|
+
# context: itself — or, when the context selects conditional sets, the
|
|
79
|
+
# variant for them, read from the store (when its slot holds the
|
|
80
|
+
# variant of this base entry and selection:
|
|
81
|
+
# ConditionalSets.variant_hash) or built read-through.
|
|
82
|
+
#
|
|
83
|
+
# @return [Selected, nil] nil for an entry that cannot be used (its
|
|
84
|
+
# conditional sets cannot be read): a miss
|
|
85
|
+
def pf_select_shared_permissions(key, entry)
|
|
86
|
+
hash, conditional, expires_at = pf_split_base_entry(entry)
|
|
87
|
+
return unless hash
|
|
88
|
+
return pf_read_permissions(hash, nil, expires_at) unless conditional
|
|
89
|
+
|
|
90
|
+
served = conditional.serve(pf_resolve_request_context(key))
|
|
91
|
+
variant = served.variant
|
|
92
|
+
return pf_read_permissions(hash, served, expires_at) unless variant
|
|
93
|
+
|
|
94
|
+
# a variant that is never stored (ConditionalSets::MAX_CACHED_SETS) is not looked for
|
|
95
|
+
stored = pf_read_shared_permissions(served.store_key(key)) if variant.cached
|
|
96
|
+
cached = PermissionForge::ConditionalSets.variant_hash(stored, variant)
|
|
97
|
+
return pf_read_permissions(cached, served, expires_at) if cached
|
|
98
|
+
|
|
99
|
+
pf_rebuild_variant_read_through(key, served, expires_at)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def pf_read_permissions(hash, served, expires_at)
|
|
103
|
+
Selected.new(permissions: hash, served: served, transaction: nil, rebuilt: nil, expires_at: expires_at)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# A stored base entry taken apart: the hash in the public format, the
|
|
107
|
+
# conditional sets it carries (ConditionalSets.split) and when the
|
|
108
|
+
# soonest assignment that went into it ends (EXPIRY_KEY).
|
|
109
|
+
#
|
|
110
|
+
# @return [Array(Hash, ConditionalSets|nil, Integer|nil), nil] nil for
|
|
111
|
+
# an entry that cannot be used
|
|
112
|
+
def pf_split_base_entry(entry)
|
|
113
|
+
expires_at = entry[EXPIRY_KEY]
|
|
114
|
+
hash, conditional = PermissionForge::ConditionalSets.split(expires_at ? entry.except(EXPIRY_KEY) : entry)
|
|
115
|
+
[hash, conditional, (expires_at if expires_at.is_a?(Integer))] if hash
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Builds the variant `served` names on a cache miss, under the same
|
|
119
|
+
# guard as a read-through rebuild of the base (the build reads this
|
|
120
|
+
# user's attributes too), and stores it unless it is withheld.
|
|
121
|
+
#
|
|
122
|
+
# @return [Selected]
|
|
123
|
+
def pf_rebuild_variant_read_through(key, served, expires_at)
|
|
124
|
+
outcome = pf_guarding_permission_build(key) do
|
|
125
|
+
transaction = PermissionForge::OpenTransaction.withholding
|
|
126
|
+
permissions = pf_rebuild_variant(key, transaction, served)
|
|
127
|
+
Selected.new(permissions: permissions, served: served, transaction: transaction, rebuilt: :variant, expires_at: expires_at)
|
|
128
|
+
end
|
|
129
|
+
outcome == :reentrant ? REENTRANT : outcome
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Builds the base entry and — when the request context selects
|
|
133
|
+
# conditional sets — the variant for them; stores each unless it is
|
|
134
|
+
# withheld inside a transaction (regenerate_permissions).
|
|
135
|
+
#
|
|
136
|
+
# @return [Selected] the hash that applies to the context, and what
|
|
137
|
+
# selected it (nil when the user holds no conditional set), as a
|
|
138
|
+
# base rebuilt
|
|
139
|
+
def pf_rebuild_permissions(key, transaction)
|
|
140
|
+
hash, conditional, expires_at = pf_split_base_entry(pf_rebuild_permission_entry(key, transaction))
|
|
141
|
+
served = conditional&.serve(pf_resolve_request_context(key))
|
|
142
|
+
hash = pf_rebuild_variant(key, transaction, served) if served&.variant
|
|
143
|
+
Selected.new(permissions: hash, served: served, transaction: transaction, rebuilt: :base, expires_at: expires_at)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Builds one entry — the base under `key` (served nil), or the variant
|
|
147
|
+
# `served` selects — and, unless it is withheld inside a transaction
|
|
148
|
+
# or not cached at all (Variant#cached), stores it, verified against
|
|
149
|
+
# the user's invalidation stamp. A variant is written only after its
|
|
150
|
+
# index entry (#pf_variant_indexed?), which is written after the
|
|
151
|
+
# stamp was read. The WithheldNotice counts permission rebuilds only
|
|
152
|
+
# (Cache::Rebuild also builds the managed-actions index).
|
|
153
|
+
#
|
|
154
|
+
# @param key [String] the user's base key
|
|
155
|
+
# @param served [ConditionalSets::Served, nil] what selected a variant
|
|
156
|
+
# @return [Hash] the entry as built, with symbol keys like one read
|
|
157
|
+
# from the store (a variant's: #pf_rebuild_variant)
|
|
158
|
+
def pf_rebuild_permission_entry(key, transaction, served = nil)
|
|
159
|
+
variant = served&.variant
|
|
160
|
+
rebuild = PermissionForge::Cache::Rebuild.new(
|
|
161
|
+
key: served ? served.store_key(key) : key, stamp_key: pf_invalidation_stamp_key, user_id: id,
|
|
162
|
+
build: -> { pf_build_permission_entry(variant) }
|
|
163
|
+
)
|
|
164
|
+
notice = PermissionForge::Cache::WithheldNotice
|
|
165
|
+
transaction ? notice.withheld : notice.not_withheld
|
|
166
|
+
withheld = transaction || (variant && !variant.cached)
|
|
167
|
+
(withheld ? rebuild.withhold : pf_store_rebuild(rebuild, key, served)).deep_symbolize_keys
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Runs a rebuild that is stored: a variant's after its index entry.
|
|
171
|
+
#
|
|
172
|
+
# @return [Hash] the entry as built
|
|
173
|
+
def pf_store_rebuild(rebuild, key, served)
|
|
174
|
+
return rebuild.call.first unless served&.variant
|
|
175
|
+
|
|
176
|
+
rebuild.call { pf_variant_indexed?(key) }.first
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# The hash a variant's rebuild built, without what its entry carries
|
|
180
|
+
# (ConditionalSets.variant_entry; nothing for a variant that is never
|
|
181
|
+
# stored).
|
|
182
|
+
def pf_rebuild_variant(key, transaction, served)
|
|
183
|
+
variant = served.variant
|
|
184
|
+
entry = pf_rebuild_permission_entry(key, transaction, served)
|
|
185
|
+
variant.cached ? PermissionForge::ConditionalSets.variant_hash(entry, variant) : entry
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Writes the index entry by which the enumerated cache:clear finds the
|
|
189
|
+
# variants of the base at `key` (Cache::VariantRemoval; erasure removes
|
|
190
|
+
# every slot without it) — after the variant's rebuild read the stamp,
|
|
191
|
+
# before the variant is written. A variant whose rebuild read the stamp
|
|
192
|
+
# before an invalidation is either verified before it, its index entry
|
|
193
|
+
# already there for the removal that follows the invalidation, or
|
|
194
|
+
# discarded by its verification. A variant whose index entry was not
|
|
195
|
+
# written is not stored: the clear would not find it. The entry
|
|
196
|
+
# outlives every variant written after it
|
|
197
|
+
# (ConditionalSets::INDEX_TTL_MARGIN).
|
|
198
|
+
#
|
|
199
|
+
# @return [Boolean] whether the store took the index entry
|
|
200
|
+
def pf_variant_indexed?(key)
|
|
201
|
+
sets = PermissionForge::ConditionalSets
|
|
202
|
+
ttl = PermissionForge.configuration.cache_ttl
|
|
203
|
+
PermissionForge.cache.write(sets.index_key(key), sets::INDEX, ttl: ttl && (ttl + sets::INDEX_TTL_MARGIN)) ? true : false
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Everything a build reads is read outside ActiveRecord's query cache.
|
|
207
|
+
# Inside a request the cache would answer the queries of a repeated
|
|
208
|
+
# build — or of a second rebuild later in the request — with the rows
|
|
209
|
+
# the first one read.
|
|
210
|
+
#
|
|
211
|
+
# The base entry (variant nil) is built from the unconditional sets and
|
|
212
|
+
# carries the conditional ones, and when the soonest assignment ends
|
|
213
|
+
# (EXPIRY_KEY); a variant from the unconditional sets plus those of the
|
|
214
|
+
# variant, and — when it is stored — carries what it was built for
|
|
215
|
+
# (ConditionalSets.variant_entry). The TTL is that of all the user's
|
|
216
|
+
# sets either way.
|
|
217
|
+
#
|
|
218
|
+
# @return [Array(Hash, Integer)] the entry and its TTL (nil for none)
|
|
219
|
+
def pf_build_permission_entry(variant)
|
|
220
|
+
PermissionForge::PermissionSet.uncached do
|
|
221
|
+
permission_sets = pf_active_non_expired_permission_sets
|
|
222
|
+
conditional = PermissionForge::ConditionalSets.of(permission_sets)
|
|
223
|
+
|
|
224
|
+
permission_data = PermissionForge::Services::PermissionHashBuilder.call(
|
|
225
|
+
user: self,
|
|
226
|
+
permission_sets: PermissionForge::ConditionalSets.build_sets(permission_sets, variant)
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
profile = PermissionForge.configuration.profile_builder.call(self)
|
|
230
|
+
|
|
231
|
+
soonest = pf_soonest_assignment_expiry
|
|
232
|
+
entry = { profile: profile, preferences: {}, permissions: permission_data }
|
|
233
|
+
if variant.nil?
|
|
234
|
+
pf_carry_in_base_entry(entry, conditional, soonest)
|
|
235
|
+
elsif variant.cached
|
|
236
|
+
entry = PermissionForge::ConditionalSets.variant_entry(entry, variant)
|
|
237
|
+
end
|
|
238
|
+
[entry, pf_permission_cache_ttl(permission_sets, soonest)]
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# What the base entry carries beside the hash: the user's conditional
|
|
243
|
+
# sets (ConditionalSets::ENTRY_KEY) and when the soonest assignment
|
|
244
|
+
# ends (EXPIRY_KEY), each only when there is one.
|
|
245
|
+
def pf_carry_in_base_entry(entry, conditional, soonest)
|
|
246
|
+
entry[PermissionForge::ConditionalSets::ENTRY_KEY] = conditional.dump if conditional
|
|
247
|
+
entry[EXPIRY_KEY] = soonest.to_i if soonest
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# The request context the activation conditions are evaluated against
|
|
251
|
+
# (config.request_context_resolver), resolved on every read of a user
|
|
252
|
+
# with conditional sets. A resolver that reads the permissions of the
|
|
253
|
+
# user it resolves for — a masked attribute of Current.user, a can_*?
|
|
254
|
+
# check — would be answered the hash being selected: one from another
|
|
255
|
+
# context, or {} (which switches row-level rules off). Neither is safe,
|
|
256
|
+
# so it raises instead — on every read of such a user.
|
|
257
|
+
#
|
|
258
|
+
# The context is a Hash; anything else the resolver returns (a Struct,
|
|
259
|
+
# an object with #to_h) is taken as its #to_h.
|
|
260
|
+
#
|
|
261
|
+
# @return [Hash]
|
|
262
|
+
# @raise [PermissionForge::ConfigurationError] when the resolver reads
|
|
263
|
+
# this user's permissions
|
|
264
|
+
def pf_resolve_request_context(key)
|
|
265
|
+
building = PermissionForge::ExecutionState.fetch(BUILDING_KEY) { {} }
|
|
266
|
+
build = building[key]
|
|
267
|
+
if build&.[](:resolving)
|
|
268
|
+
raise PermissionForge::ConfigurationError,
|
|
269
|
+
"config.request_context_resolver read the permissions of the user it resolves the context for (#{key}): " \
|
|
270
|
+
'a resolver must not read permissions'
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
own = build.nil?
|
|
274
|
+
build = building[key] = {} if own
|
|
275
|
+
build[:resolving] = true
|
|
276
|
+
begin
|
|
277
|
+
pf_context_hash(PermissionForge.configuration.request_context_resolver.call)
|
|
278
|
+
ensure
|
|
279
|
+
own ? building.delete(key) : build.delete(:resolving)
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def pf_context_hash(context)
|
|
284
|
+
context.is_a?(Hash) ? context : context.to_h
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module PermissionForge
|
|
2
|
+
module Concerns
|
|
3
|
+
# Invalidates the cached permission hash of every user affected by a change
|
|
4
|
+
# to the including permission model. The rebuild happens lazily on each
|
|
5
|
+
# user's next permissions_hash read (read-through in PermissionCacheAccessor),
|
|
6
|
+
# so activation conditions are evaluated in the affected user's own request
|
|
7
|
+
# context — not the context of whoever edited the permission records.
|
|
8
|
+
#
|
|
9
|
+
# Runs after commit through InvalidatesInBatch: the records written in one
|
|
10
|
+
# transaction invalidate each affected user once (per tenant), not once per
|
|
11
|
+
# record.
|
|
12
|
+
#
|
|
13
|
+
# @api private
|
|
14
|
+
module PermissionsRegenerator
|
|
15
|
+
extend ActiveSupport::Concern
|
|
16
|
+
include InvalidatesInBatch
|
|
17
|
+
|
|
18
|
+
# NOTE: The including model must define a private method `users_on_permission_set`
|
|
19
|
+
# that returns a collection of PermissionForge::PermissionSetUser records
|
|
20
|
+
# (or `user_ids_on_permission_set`, the ids of their users).
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def invalidate_committed_caches
|
|
25
|
+
super
|
|
26
|
+
invalidate_permissions_for_users
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def invalidate_permissions_for_users
|
|
30
|
+
invalidate_assigned_users(user_ids_on_permission_set, invalidation_batch, permission_record_tenant_id)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# The user ids of users_on_permission_set — plucked from a relation,
|
|
34
|
+
# without loading a row.
|
|
35
|
+
def user_ids_on_permission_set
|
|
36
|
+
holders = users_on_permission_set
|
|
37
|
+
return holders.distinct.pluck(:user_id) if holders.is_a?(ActiveRecord::Relation)
|
|
38
|
+
|
|
39
|
+
holders.filter_map(&:user_id).uniq
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Concerns
|
|
5
|
+
# Writes one audit event per create, update and destroy of a permission
|
|
6
|
+
# record (PermissionForge::Audit.record) — from after_create, after_update
|
|
7
|
+
# and after_destroy, inside the record's own transaction, so a change
|
|
8
|
+
# whose event cannot be written rolls back under the default
|
|
9
|
+
# `config.audit_change_failure_strategy = :raise`.
|
|
10
|
+
#
|
|
11
|
+
# What an event carries:
|
|
12
|
+
# create — data["snapshot"]: every attribute of the new row
|
|
13
|
+
# update — data["changes"]: saved_changes minus the bookkeeping columns
|
|
14
|
+
# (created_at, updated_at, created_by_id, updated_by_id); a
|
|
15
|
+
# save that changed nothing else writes no event. A model
|
|
16
|
+
# adds keys of its own through #audit_update_details (an
|
|
17
|
+
# assignment moved to another user: previous_subject_user_id)
|
|
18
|
+
# destroy — data["snapshot"]: every attribute of the deleted row
|
|
19
|
+
# plus the record reference (#audit_reference): type, id, uuid, label, the
|
|
20
|
+
# Permission Set, the subject user of an assignment or membership, and
|
|
21
|
+
# the record's tenant. Everything written in one transaction shares a
|
|
22
|
+
# correlation id (a cascade delete, a Review-and-save).
|
|
23
|
+
#
|
|
24
|
+
# A caller that means something more specific by a save renames and
|
|
25
|
+
# enriches the row's own event instead of adding a second one:
|
|
26
|
+
#
|
|
27
|
+
# assignment.audit_as('assignment.reviewed', data: { 'decision' => 'keep' }, reason: reason) do
|
|
28
|
+
# assignment.update!(reviewed_at: Time.current, reviewed_by_id: admin.id)
|
|
29
|
+
# end
|
|
30
|
+
#
|
|
31
|
+
# Paths that run no callbacks write no event: touch, update_column(s),
|
|
32
|
+
# update_all, delete_all, delete, insert_all, upsert_all. The gem's own
|
|
33
|
+
# code does not use them on these models; a test guards against new uses.
|
|
34
|
+
#
|
|
35
|
+
# Nothing is kept on the record outside a callback or an audit_as block:
|
|
36
|
+
# it can still be marshalled, and its associations are left as they were.
|
|
37
|
+
#
|
|
38
|
+
# @api private
|
|
39
|
+
module RecordsAuditEvents
|
|
40
|
+
extend ActiveSupport::Concern
|
|
41
|
+
|
|
42
|
+
BOOKKEEPING_COLUMNS = %w[created_at updated_at created_by_id updated_by_id].freeze
|
|
43
|
+
VERBS = { create: 'created', update: 'updated', destroy: 'destroyed' }.freeze
|
|
44
|
+
|
|
45
|
+
# Models whose events have their own names in the catalogue, by
|
|
46
|
+
# model key (base_class.model_name.element). Every other model's events
|
|
47
|
+
# are "<key>.created|updated|destroyed".
|
|
48
|
+
EVENT_NAMES = {
|
|
49
|
+
'permission_set_user' => { create: 'assignment.granted', update: 'assignment.changed', destroy: 'assignment.revoked' },
|
|
50
|
+
'permission_set_group_user' => { create: 'group_membership.added', update: 'group_membership.changed',
|
|
51
|
+
destroy: 'group_membership.removed' }
|
|
52
|
+
}.freeze
|
|
53
|
+
|
|
54
|
+
# The Permission Set an event about the record belongs to, by model key.
|
|
55
|
+
# Groups and memberships belong to none. Associations are read through
|
|
56
|
+
# #audit_associated: as loaded, else fetched in the record's own tenant.
|
|
57
|
+
OWN_SET = ->(record) { record.audit_associated(:permission_set) }
|
|
58
|
+
SET_OF_RESOURCE = ->(record) { record.audit_associated(:resource)&.audit_permission_set }
|
|
59
|
+
PERMISSION_SET = {
|
|
60
|
+
'permission_set' => ->(record) { record },
|
|
61
|
+
'resource' => OWN_SET, 'permission_set_rule' => OWN_SET, 'permission_set_user' => OWN_SET,
|
|
62
|
+
'resource_field' => SET_OF_RESOURCE, 'resource_action' => SET_OF_RESOURCE
|
|
63
|
+
}.freeze
|
|
64
|
+
|
|
65
|
+
# How the record is labelled, by model key; the default is its name.
|
|
66
|
+
QUALIFIED_NAME = ->(record) { [record.audit_associated(:resource)&.name, record.name].compact.join('.') }
|
|
67
|
+
PERSON_NAME = lambda { |record|
|
|
68
|
+
user = record.audit_associated(:user)
|
|
69
|
+
PermissionForge::Audit::Actor.label_for(user) if user
|
|
70
|
+
}
|
|
71
|
+
LABELS = {
|
|
72
|
+
'resource_field' => QUALIFIED_NAME, 'resource_action' => QUALIFIED_NAME,
|
|
73
|
+
'permission_set_user' => PERSON_NAME, 'permission_set_group_user' => PERSON_NAME
|
|
74
|
+
}.freeze
|
|
75
|
+
|
|
76
|
+
# The names #audit_as may give a model's own events, by model key and
|
|
77
|
+
# action — a more specific name for the same row change, never another
|
|
78
|
+
# model's event. Adding one is one entry.
|
|
79
|
+
AUDIT_AS_RENAMES = {
|
|
80
|
+
'permission_set_user' => { update: %w[assignment.reviewed] },
|
|
81
|
+
'resource_field' => { update: %w[field.sensitive_acknowledged] }
|
|
82
|
+
}.freeze
|
|
83
|
+
|
|
84
|
+
# Keys of an event's data that the concern computes (#audit_as cannot set them).
|
|
85
|
+
AUDIT_AS_COMPUTED_KEYS = %w[changes snapshot].freeze
|
|
86
|
+
|
|
87
|
+
included do
|
|
88
|
+
after_create :record_audit_event_for_create
|
|
89
|
+
after_update :record_audit_event_for_update
|
|
90
|
+
after_destroy :record_audit_event_for_destroy
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
class_methods do
|
|
94
|
+
# @param action [Symbol] :create, :update or :destroy
|
|
95
|
+
# @return [String] the catalogue name of the event
|
|
96
|
+
def audit_event_name(action)
|
|
97
|
+
action = action.to_sym
|
|
98
|
+
EVENT_NAMES.dig(audit_model_key, action) || "#{audit_model_key}.#{VERBS.fetch(action)}"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# 'permission_set', 'resource_field', ... — also for a subclass.
|
|
102
|
+
def audit_model_key
|
|
103
|
+
base_class.model_name.element
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Records this record's events inside the block under `event` instead
|
|
108
|
+
# of their own name, with `data` added to theirs and `reason` on them —
|
|
109
|
+
# only for the actions AUDIT_AS_RENAMES lists for the model and that
|
|
110
|
+
# event (a review or an acknowledgement is an update); any other
|
|
111
|
+
# action inside the block keeps its own event. Instance-scoped,
|
|
112
|
+
# block-only, restored when the block ends or raises. Nothing is set
|
|
113
|
+
# when the arguments are refused.
|
|
114
|
+
#
|
|
115
|
+
# @param event [String] a change event the model may be renamed to
|
|
116
|
+
# @param data [Hash] event-specific keys; the computed 'changes' /
|
|
117
|
+
# 'snapshot' cannot be replaced
|
|
118
|
+
# @param reason [String, nil]
|
|
119
|
+
# @return the value of the block
|
|
120
|
+
# @raise [Audit::EventCatalog::UnknownEventError, ArgumentError]
|
|
121
|
+
def audit_as(event, data: {}, reason: nil)
|
|
122
|
+
override = audit_as_override(event, data, reason) # raises before anything is set
|
|
123
|
+
previous = @pf_audit_as
|
|
124
|
+
@pf_audit_as = override
|
|
125
|
+
begin
|
|
126
|
+
yield
|
|
127
|
+
ensure
|
|
128
|
+
@pf_audit_as = previous
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# The columns an event about this record carries (Audit::Recorder).
|
|
133
|
+
#
|
|
134
|
+
# @return [Hash] record_type, record_id, record_uuid, record_label,
|
|
135
|
+
# permission_set_id, permission_set_uuid, subject_user_id, tenant_id
|
|
136
|
+
def audit_reference
|
|
137
|
+
with_audit_lookups do
|
|
138
|
+
set = audit_permission_set
|
|
139
|
+
{
|
|
140
|
+
record_type: self.class.base_class.name, record_id: id&.to_s, record_uuid: audit_uuid, record_label: audit_record_label,
|
|
141
|
+
permission_set_id: set&.id, permission_set_uuid: set&.uuid,
|
|
142
|
+
subject_user_id: audit_subject_user_id, tenant_id: audit_tenant_id
|
|
143
|
+
}
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Every attribute, JSON-ready.
|
|
148
|
+
def audit_snapshot
|
|
149
|
+
attributes.as_json
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# saved_changes minus the bookkeeping columns, JSON-ready.
|
|
153
|
+
def audit_changes
|
|
154
|
+
saved_changes.except(*BOOKKEEPING_COLUMNS).as_json
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# The Permission Set an event about this record belongs to, if any.
|
|
158
|
+
def audit_permission_set
|
|
159
|
+
PERMISSION_SET[self.class.audit_model_key]&.call(self)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# A belongs_to target as loaded, else fetched in this record's tenant
|
|
163
|
+
# and past every default scope — the ambient tenant may be another one
|
|
164
|
+
# or none (acts_as_tenant then raises under require_tenant), and the
|
|
165
|
+
# host's user model may hide the person behind a soft delete. What is
|
|
166
|
+
# fetched is not written to the association (the record would then
|
|
167
|
+
# answer with a hidden user, or with nothing for a row that is gone);
|
|
168
|
+
# inside #audit_reference it is looked up once.
|
|
169
|
+
#
|
|
170
|
+
# @param name [Symbol] a belongs_to association
|
|
171
|
+
# @return [ActiveRecord::Base, nil]
|
|
172
|
+
def audit_associated(name)
|
|
173
|
+
association = association(name)
|
|
174
|
+
return association.target if association.loaded?
|
|
175
|
+
return @pf_audit_lookups[name] if @pf_audit_lookups&.key?(name)
|
|
176
|
+
|
|
177
|
+
id = self[association.reflection.foreign_key]
|
|
178
|
+
found = id.nil? ? nil : audit_fetch(association.klass, id)
|
|
179
|
+
@pf_audit_lookups[name] = found if @pf_audit_lookups
|
|
180
|
+
found
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
private
|
|
184
|
+
|
|
185
|
+
# The validated #audit_as state: the event, the actions it renames, the
|
|
186
|
+
# data and the reason.
|
|
187
|
+
def audit_as_override(event, data, reason)
|
|
188
|
+
entry = PermissionForge::Audit::EventCatalog.fetch(event)
|
|
189
|
+
raise ArgumentError, "#{entry.name} is a #{entry.category} event: audit_as takes a change event" unless entry.category == 'change'
|
|
190
|
+
|
|
191
|
+
{ event: entry.name, actions: audit_as_actions(entry.name), data: audit_as_data(data), reason: reason }
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# The actions of this model that may be recorded as `name` (AUDIT_AS_RENAMES).
|
|
195
|
+
def audit_as_actions(name)
|
|
196
|
+
key = self.class.audit_model_key
|
|
197
|
+
actions = AUDIT_AS_RENAMES.fetch(key, {}).select { |_, names| names.include?(name) }.keys
|
|
198
|
+
raise ArgumentError, "#{key} events cannot be recorded as #{name}" if actions.empty?
|
|
199
|
+
|
|
200
|
+
actions
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def audit_as_data(data)
|
|
204
|
+
data = data.as_json
|
|
205
|
+
reserved = data.keys & AUDIT_AS_COMPUTED_KEYS
|
|
206
|
+
raise ArgumentError, "audit_as data cannot set #{reserved.join(', ')}" if reserved.any?
|
|
207
|
+
|
|
208
|
+
data
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Memoizes association lookups for the duration of the block only.
|
|
212
|
+
def with_audit_lookups
|
|
213
|
+
@pf_audit_lookups = {}
|
|
214
|
+
yield
|
|
215
|
+
ensure
|
|
216
|
+
@pf_audit_lookups = nil
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def audit_fetch(klass, id)
|
|
220
|
+
scope = klass.respond_to?(:of_tenant) ? klass.of_tenant(audit_tenant_id) : klass.unscoped
|
|
221
|
+
PermissionForge::Concerns::AutoPermissionScoping.internally_suspended { scope.find_by(klass.primary_key => id) }
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def record_audit_event_for_create
|
|
225
|
+
record_audit_event(:create, 'snapshot' => audit_snapshot)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def record_audit_event_for_update
|
|
229
|
+
changes = audit_changes
|
|
230
|
+
return if changes.empty?
|
|
231
|
+
|
|
232
|
+
record_audit_event(:update, audit_update_details.merge('changes' => changes))
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Event data a model adds to its update events (the computed 'changes' wins).
|
|
236
|
+
def audit_update_details
|
|
237
|
+
{}
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def record_audit_event_for_destroy
|
|
241
|
+
return unless audit_row_deleted?
|
|
242
|
+
|
|
243
|
+
record_audit_event(:destroy, 'snapshot' => audit_snapshot)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Rails runs after_destroy also when the DELETE hit no row — an instance
|
|
247
|
+
# destroyed before, or already gone through a cascade. Nothing changed
|
|
248
|
+
# then, and the event for the row was written when it did go. Rails
|
|
249
|
+
# keeps the answer for its own commit callbacks (7.1 to 8.1).
|
|
250
|
+
def audit_row_deleted?
|
|
251
|
+
return true unless respond_to?(:_trigger_destroy_callback, true)
|
|
252
|
+
|
|
253
|
+
send(:_trigger_destroy_callback) ? true : false
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# The row's own event, or the one an enclosing audit_as block asked for.
|
|
257
|
+
def record_audit_event(action, data)
|
|
258
|
+
override = @pf_audit_as if @pf_audit_as&.fetch(:actions)&.include?(action)
|
|
259
|
+
event = override ? override[:event] : self.class.audit_event_name(action)
|
|
260
|
+
data = override[:data].merge(data) if override # the computed keys win
|
|
261
|
+
PermissionForge::Audit.record(event, record: self, data: data, reason: override&.fetch(:reason))
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def audit_uuid
|
|
265
|
+
has_attribute?(:uuid) ? self[:uuid] : nil
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def audit_record_label
|
|
269
|
+
label = LABELS[self.class.audit_model_key]
|
|
270
|
+
label ? label.call(self) : try(:name)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def audit_subject_user_id
|
|
274
|
+
user_id&.to_s if respond_to?(:user_id)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def audit_tenant_id
|
|
278
|
+
config = PermissionForge.configuration
|
|
279
|
+
return unless config.tenant_enabled?
|
|
280
|
+
|
|
281
|
+
column = config.resolved_tenant_column
|
|
282
|
+
self[column] if has_attribute?(column)
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|