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,178 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Audit
|
|
5
|
+
# The runtime decision events of the gem — what each one carries — for
|
|
6
|
+
# the code that makes the decisions (PermissionScopedFindable,
|
|
7
|
+
# AutoPermissionScoping, ControllerActionGuard, the services). Every
|
|
8
|
+
# method returns at once, before anything is built, when the event is
|
|
9
|
+
# neither stored nor heard (Audit.instrument?).
|
|
10
|
+
#
|
|
11
|
+
# Payloads name the model and the record by class (record_type: the base
|
|
12
|
+
# class) and id only — never through a host record's readers, which a
|
|
13
|
+
# masked model guards. The user the decision was made for is
|
|
14
|
+
# subject_user_id, the actor is Actor.current (the request's actor), and
|
|
15
|
+
# the live user object goes to in-process subscribers as
|
|
16
|
+
# payload[:objects][:user].
|
|
17
|
+
#
|
|
18
|
+
# What happens on every query under auto-scoping is published at most once
|
|
19
|
+
# per request (an executor run) per event, model, reason and user (#once?):
|
|
20
|
+
# the memo lives in the request's isolated state (ExecutionState), never
|
|
21
|
+
# on a record, is emptied with the audit context at the end of every
|
|
22
|
+
# executor run that started without it (a request, a job) and by
|
|
23
|
+
# Audit.reset!.
|
|
24
|
+
#
|
|
25
|
+
# @api private
|
|
26
|
+
module Runtime
|
|
27
|
+
BYPASSED = 'rule.bypassed'
|
|
28
|
+
DENIED = 'access.denied'
|
|
29
|
+
ACTION_DENIED = 'action.denied'
|
|
30
|
+
UNSCOPED = 'query.unscoped'
|
|
31
|
+
|
|
32
|
+
# Why row-level rules (or the action guard) did not apply.
|
|
33
|
+
BYPASS_REASONS = %w[view_all modify_all nil_user scope_suspended auto_scope_opt_out no_rules guard_bypass].freeze
|
|
34
|
+
|
|
35
|
+
# Asked on every relation built under auto-scoping.
|
|
36
|
+
BYPASS_GATE = Gate.for(BYPASSED)
|
|
37
|
+
|
|
38
|
+
ONCE_KEY = :pf_audit_runtime_once
|
|
39
|
+
# A thread that never ends an executor run (a script, a daemon loop)
|
|
40
|
+
# forgets what it published past this many keys.
|
|
41
|
+
ONCE_LIMIT = 1_000
|
|
42
|
+
|
|
43
|
+
class << self
|
|
44
|
+
# rule.bypassed: the rules (or the guard) were skipped, and why.
|
|
45
|
+
#
|
|
46
|
+
# @param reason [String, Symbol] one of BYPASS_REASONS
|
|
47
|
+
# @param model [Class, String] the model (or Resource name)
|
|
48
|
+
# @param via [String] find_by, permission_scoped, auto_scope, rule_applier, action_guard
|
|
49
|
+
# @param user [Object, nil] the user the decision was made for
|
|
50
|
+
# @param once [Boolean] at most once per request
|
|
51
|
+
# @param data [Hash] more event data (string keys)
|
|
52
|
+
# @raise [ArgumentError] for a reason outside BYPASS_REASONS
|
|
53
|
+
def bypassed(reason, model:, via:, user: nil, once: false, data: nil) # rubocop:disable Metrics/ParameterLists
|
|
54
|
+
return unless BYPASS_GATE.open? && Audit.instrument?(BYPASSED)
|
|
55
|
+
|
|
56
|
+
reason = reason.to_s
|
|
57
|
+
raise ArgumentError, "unknown bypass reason #{reason.inspect} (#{BYPASS_REASONS.join(', ')})" unless BYPASS_REASONS.include?(reason)
|
|
58
|
+
return if once && !once?(BYPASSED, model, reason, via, user)
|
|
59
|
+
|
|
60
|
+
publish(BYPASSED, model: model, user: user, record_id: nil,
|
|
61
|
+
data: { 'reason' => reason, 'via' => via }.merge(data || {}))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# access.denied (rows of a model) or action.denied (a controller
|
|
65
|
+
# action): the decision, why, and the record when it is known.
|
|
66
|
+
#
|
|
67
|
+
# @param event [String] DENIED or ACTION_DENIED
|
|
68
|
+
# @param reason [String, Symbol] why (rule, nil_user, a guard reason)
|
|
69
|
+
# @param model [Class, String]
|
|
70
|
+
# @param via [String]
|
|
71
|
+
# @param user [Object, nil]
|
|
72
|
+
# @param record_id [Object, nil] the denied record's id, when known
|
|
73
|
+
# @param once [Boolean] at most once per request
|
|
74
|
+
# @param data [Hash] more event data (string keys)
|
|
75
|
+
def denied(event, reason:, model:, via:, user: nil, record_id: nil, once: false, data: nil) # rubocop:disable Metrics/ParameterLists
|
|
76
|
+
return unless Audit.instrument?(event)
|
|
77
|
+
return if once && !once?(event, model, reason.to_s, via, user)
|
|
78
|
+
|
|
79
|
+
publish(event, model: model, user: user, record_id: record_id,
|
|
80
|
+
data: { 'reason' => reason.to_s, 'via' => via }.merge(data || {}))
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# query.unscoped: Level 2 detection saw a query on a protected table
|
|
84
|
+
# that went past the rules. Once per request per table.
|
|
85
|
+
#
|
|
86
|
+
# @param model [Class, String, nil] the protected model, when known
|
|
87
|
+
# @param table [String]
|
|
88
|
+
# @param data [Hash]
|
|
89
|
+
def unscoped(model:, table:, data: nil)
|
|
90
|
+
return unless Audit.instrument?(UNSCOPED)
|
|
91
|
+
return unless once?(UNSCOPED, model || table, table, nil, nil)
|
|
92
|
+
|
|
93
|
+
user = current_user
|
|
94
|
+
publish(UNSCOPED, model: model || table, user: user, record_id: nil, data: { 'table' => table }.merge(data || {}))
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# permissions.rebuilt (the `cache` group): a user's permission hash
|
|
98
|
+
# was built — read through on a cache miss, or asked for — and whether
|
|
99
|
+
# it was withheld from the cache inside a transaction.
|
|
100
|
+
def rebuilt(user, read_through:, withheld:)
|
|
101
|
+
cache_event('permissions.rebuilt', user, 'read_through' => read_through, 'withheld' => withheld)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# permissions.invalidated (the `cache` group): a user's cached hash
|
|
105
|
+
# was removed, for which tenant, and whether the store confirmed it.
|
|
106
|
+
def invalidated(user, tenant_id:, confirmed:)
|
|
107
|
+
cache_event('permissions.invalidated', user, 'tenant_id' => tenant_id, 'confirmed' => confirmed)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Whether this is the first time in this request that `key` was
|
|
111
|
+
# published; remembers it.
|
|
112
|
+
def once?(*key)
|
|
113
|
+
key[-1] = user_id(key[-1])
|
|
114
|
+
seen = (state[ONCE_KEY] ||= {})
|
|
115
|
+
return false if seen.key?(key)
|
|
116
|
+
|
|
117
|
+
seen.clear if seen.size >= ONCE_LIMIT
|
|
118
|
+
seen[key] = true
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Forgets what was published once (end of an executor run, tests).
|
|
122
|
+
def reset!
|
|
123
|
+
state.delete(ONCE_KEY)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# The user's id, read as its primary key; nil for no user or an
|
|
127
|
+
# object that has none.
|
|
128
|
+
def user_id(user)
|
|
129
|
+
user.id if user.respond_to?(:id)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# The model's base class name (a Resource name as given).
|
|
133
|
+
def record_type(model)
|
|
134
|
+
return model.to_s unless model.is_a?(Class)
|
|
135
|
+
|
|
136
|
+
model.respond_to?(:base_class) ? model.base_class.name : model.name
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# The model's own name (a subclass stays itself).
|
|
140
|
+
def model_name(model)
|
|
141
|
+
model.is_a?(Class) ? model.name : model.to_s
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private
|
|
145
|
+
|
|
146
|
+
# Whose hash: record and subject are the user.
|
|
147
|
+
def cache_event(event, user, data)
|
|
148
|
+
return unless Audit.instrument?(event)
|
|
149
|
+
|
|
150
|
+
Audit.instrument(event) do
|
|
151
|
+
{ record_type: record_type(user.class), record_id: user_id(user), subject_user_id: user_id(user), data: data,
|
|
152
|
+
objects: { user: user } }
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def publish(event, model:, user:, record_id:, data:)
|
|
157
|
+
Audit.instrument(event) do
|
|
158
|
+
{ record_type: record_type(model), record_id: record_id, subject_user_id: user_id(user),
|
|
159
|
+
data: { 'model' => model_name(model) }.merge(data), objects: { user: user } }
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# A user the resolver raises for is no user here: the decision being
|
|
164
|
+
# reported has been made already, and Actor.current reports the
|
|
165
|
+
# resolver's failure when the event is built.
|
|
166
|
+
def current_user
|
|
167
|
+
PermissionForge.current_user
|
|
168
|
+
rescue StandardError
|
|
169
|
+
nil
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def state
|
|
173
|
+
ExecutionState
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Audit
|
|
5
|
+
# How Audit.instrument emits a runtime (access) event once it knows the
|
|
6
|
+
# event is stored or heard. Extended into Audit; uses its #write, #notify
|
|
7
|
+
# and #listening?.
|
|
8
|
+
#
|
|
9
|
+
# Published at once — the decision has been made, whatever becomes of the
|
|
10
|
+
# transaction it was made in — never after a commit. Stored at once, or,
|
|
11
|
+
# inside an open transaction, once the outermost transaction has ended,
|
|
12
|
+
# committed or rolled back (a denial inside a transaction the denial
|
|
13
|
+
# itself rolls back is still recorded); the notification then says
|
|
14
|
+
# `stored: false, deferred: true`.
|
|
15
|
+
#
|
|
16
|
+
# Re-entrancy: nothing is done while this thread (fiber) is already
|
|
17
|
+
# building, storing or publishing a runtime event (#emitting?). A payload
|
|
18
|
+
# or a subscriber that reads a masked attribute or runs a permission
|
|
19
|
+
# check gets its answer, but publishes no event about it — and cannot
|
|
20
|
+
# loop.
|
|
21
|
+
#
|
|
22
|
+
# @api private
|
|
23
|
+
module RuntimeEmission
|
|
24
|
+
# Set while this fiber builds, stores or publishes a runtime event — a
|
|
25
|
+
# fiber-local ExecutionState slot: the emission runs in the fiber that
|
|
26
|
+
# started it.
|
|
27
|
+
EMITTING_KEY = :pf_audit_emitting
|
|
28
|
+
|
|
29
|
+
# @return [Boolean] whether this fiber is building, storing or
|
|
30
|
+
# publishing a runtime event
|
|
31
|
+
def emitting?
|
|
32
|
+
ExecutionState[EMITTING_KEY] == true
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Hands { data:, objects: } to the gem's light subscribers
|
|
36
|
+
# (Gate#direct_listeners) with the re-entrancy guard, building no event.
|
|
37
|
+
# For a caller that has the payload at hand (Audit::FieldAccess).
|
|
38
|
+
#
|
|
39
|
+
# @return [nil]
|
|
40
|
+
def emit_light(entry, listeners, light)
|
|
41
|
+
return if emitting?
|
|
42
|
+
|
|
43
|
+
emitting { deliver_light(entry, listeners, light) }
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# One field decision handed to the light subscribers (the legacy
|
|
48
|
+
# on_field_filtered hook) as it is made (Audit::FieldAccess::Direct).
|
|
49
|
+
#
|
|
50
|
+
# @return [nil]
|
|
51
|
+
def emit_decision(listeners, resource_name, field_name, action, user)
|
|
52
|
+
return if emitting?
|
|
53
|
+
|
|
54
|
+
emitting do
|
|
55
|
+
listeners.each { |listener| listener.deliver_decision(resource_name, field_name, action, user) }
|
|
56
|
+
end
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
# The legacy hooks are the only listeners and nothing is stored
|
|
63
|
+
# (Gate#direct_listeners): they get { data:, objects: } straight away —
|
|
64
|
+
# no Recorder, no actor, no notification.
|
|
65
|
+
#
|
|
66
|
+
# @return [nil]
|
|
67
|
+
def instrument_light(entry, listeners, payload)
|
|
68
|
+
return if emitting?
|
|
69
|
+
|
|
70
|
+
emitting do
|
|
71
|
+
payload = runtime_payload(entry, payload) { yield if block_given? }
|
|
72
|
+
unless payload.equal?(FAILED)
|
|
73
|
+
light = { event: entry.name, data: payload[:data] || {}, objects: payload[:objects] || Recorder::NO_OBJECTS }
|
|
74
|
+
deliver_light(entry, listeners, light)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def deliver_light(entry, listeners, light)
|
|
81
|
+
listeners.each { |listener| listener.deliver_light(entry.notification, light) }
|
|
82
|
+
rescue StandardError => e
|
|
83
|
+
Failure.handle(e, strategy: configuration.audit_hook_failure_strategy, operation: :publish, event: entry.name)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# @return [AuditEvent, nil]
|
|
87
|
+
def instrument_runtime(entry, payload, store:)
|
|
88
|
+
return if emitting?
|
|
89
|
+
|
|
90
|
+
emitting do
|
|
91
|
+
payload = runtime_payload(entry, payload) { yield if block_given? }
|
|
92
|
+
emit_runtime(Recorder.new(entry, payload), store: store) unless payload.equal?(FAILED)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def emitting(&)
|
|
97
|
+
ExecutionState.with(EMITTING_KEY, true, &)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# The caller's payload with the block's merged in, source 'runtime'
|
|
101
|
+
# unless given. A block that raises follows audit_hook_failure_strategy.
|
|
102
|
+
def runtime_payload(entry, payload)
|
|
103
|
+
more = yield
|
|
104
|
+
payload = payload.merge(more) if more
|
|
105
|
+
payload.merge(source: payload[:source] || 'runtime')
|
|
106
|
+
rescue StandardError => e
|
|
107
|
+
Failure.handle(e, strategy: configuration.audit_hook_failure_strategy, operation: :build, event: entry.name)
|
|
108
|
+
FAILED
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def emit_runtime(recorder, store:)
|
|
112
|
+
transaction = OpenTransaction.current(PermissionForge::AuditEvent) if store
|
|
113
|
+
stored = write(recorder, store: store && transaction.nil?, strategy: configuration.audit_hook_failure_strategy,
|
|
114
|
+
change: false)
|
|
115
|
+
return if stored.equal?(FAILED)
|
|
116
|
+
|
|
117
|
+
store_runtime_later(transaction, recorder) if transaction
|
|
118
|
+
notify(recorder.entry, recorder.notification_payload(stored, deferred: !transaction.nil?)) if listening?(recorder.entry)
|
|
119
|
+
stored
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# OpenTransaction#after_end: once, when the outermost transaction has
|
|
123
|
+
# ended either way (a savepoint that rolls back hands it on). It runs
|
|
124
|
+
# from Rails' commit or rollback, where raising would replace the
|
|
125
|
+
# caller's own exception: a failure is reported, never raised, whatever
|
|
126
|
+
# the strategy (as Audit::Detached does).
|
|
127
|
+
def store_runtime_later(transaction, recorder)
|
|
128
|
+
transaction.after_end do
|
|
129
|
+
strategy = configuration.audit_hook_failure_strategy
|
|
130
|
+
emitting { write(recorder, store: true, strategy: strategy == :raise ? :report : strategy, change: false) }
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Audit
|
|
5
|
+
# Records an `audit.settings_observed` event when the audit settings in
|
|
6
|
+
# force differ from those of the most recent such event, so "was auditing
|
|
7
|
+
# ever off, and since when" can be answered from the log. The engine calls
|
|
8
|
+
# it at boot (not in the test environment); it never raises.
|
|
9
|
+
#
|
|
10
|
+
# Only the settings at boot are observed: a toggle made at runtime after
|
|
11
|
+
# boot is not seen until the next boot or an explicit call.
|
|
12
|
+
#
|
|
13
|
+
# Lambdas are recorded as "set" / "unset" only, never their code.
|
|
14
|
+
#
|
|
15
|
+
# PermissionForge::Audit::SettingsObserver.call # => AuditEvent | nil
|
|
16
|
+
#
|
|
17
|
+
# @api private
|
|
18
|
+
module SettingsObserver
|
|
19
|
+
EVENT = 'audit.settings_observed'
|
|
20
|
+
OPTIONS = %i[
|
|
21
|
+
audit_events audit_actor_resolver audit_request_context audit_change_failure_strategy
|
|
22
|
+
audit_hook_failure_strategy audit_runtime_events audit_log_subscriber audit_retention_period
|
|
23
|
+
audit_log_authorization admin_assignment_authorization
|
|
24
|
+
].freeze
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
# @return [AuditEvent, nil] the new event; nil when nothing changed,
|
|
28
|
+
# the table is missing, auditing is off or recording failed
|
|
29
|
+
def call(configuration: PermissionForge.configuration)
|
|
30
|
+
return unless PermissionForge::AuditEvent.table_exists?
|
|
31
|
+
|
|
32
|
+
settings = fingerprint(configuration)
|
|
33
|
+
return if last_observed == settings
|
|
34
|
+
|
|
35
|
+
payload = { data: { 'settings' => settings }, actor: { type: 'system', label: 'boot' }, source: 'service' }
|
|
36
|
+
return PermissionForge::Audit.record(EVENT, **payload) unless configuration.audit_events == false
|
|
37
|
+
|
|
38
|
+
# Auditing is off: Audit.record would store nothing, yet "was auditing
|
|
39
|
+
# ever off" is what this event answers. Stored directly, not published.
|
|
40
|
+
Recorder.new(EventCatalog.fetch(EVENT), payload).store!
|
|
41
|
+
rescue StandardError => e
|
|
42
|
+
Rails.logger&.warn("[PermissionForge] could not record the audit settings: #{e.class}: #{e.message}") if defined?(Rails)
|
|
43
|
+
nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @return [Hash{String => Object}] stable, JSON-safe values
|
|
47
|
+
def fingerprint(configuration = PermissionForge.configuration)
|
|
48
|
+
OPTIONS.to_h { |option| [option.to_s, value_of(configuration.public_send(option))] }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def value_of(value)
|
|
54
|
+
case value
|
|
55
|
+
when nil then 'unset'
|
|
56
|
+
when Array then value.map(&:to_s).sort
|
|
57
|
+
when ActiveSupport::Duration, Numeric then value.to_i
|
|
58
|
+
when Symbol then value.to_s
|
|
59
|
+
when String, true, false then value
|
|
60
|
+
else 'set' # a lambda, or anything else: only that it is configured
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def last_observed
|
|
65
|
+
PermissionForge::AuditEvent.unscoped.where(event: EVENT).order(occurred_at: :desc, id: :desc).first&.data&.[]('settings')
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Audit
|
|
5
|
+
# Where an event came from: the source column. The admin UI, the API and
|
|
6
|
+
# jobs say so through the audit context (Audit::Context); a process that
|
|
7
|
+
# says nothing is a console, a rake task, or else a service.
|
|
8
|
+
#
|
|
9
|
+
# @api private
|
|
10
|
+
module Source
|
|
11
|
+
SOURCES = %w[admin_ui api rake job console service runtime].freeze
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
# @return [String] the context's source, else the detected one
|
|
15
|
+
def current
|
|
16
|
+
Context[:source] || detect
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @param console [Boolean] whether this process is `rails console`
|
|
20
|
+
# @param rake_tasks [Array<String>] the tasks rake was started with
|
|
21
|
+
# @return [String]
|
|
22
|
+
def detect(console: process_console?, rake_tasks: process_rake_tasks)
|
|
23
|
+
return 'console' if console
|
|
24
|
+
return 'rake' if rake_tasks.any?
|
|
25
|
+
|
|
26
|
+
'service'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @raise [ArgumentError] for a source outside the contract
|
|
30
|
+
# @return [String]
|
|
31
|
+
def validate!(value)
|
|
32
|
+
source = value.to_s
|
|
33
|
+
raise ArgumentError, "audit source must be one of #{SOURCES.join(', ')} (got #{value.inspect})" unless SOURCES.include?(source)
|
|
34
|
+
|
|
35
|
+
source
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def process_console?
|
|
41
|
+
defined?(::Rails::Console) ? true : false
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Rake is loaded in a test process too: only a process started with a
|
|
45
|
+
# task is a rake task.
|
|
46
|
+
def process_rake_tasks
|
|
47
|
+
return [] unless defined?(::Rake::Application) && ::Rake.respond_to?(:application)
|
|
48
|
+
|
|
49
|
+
::Rake.application.top_level_tasks
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Audit
|
|
5
|
+
module Subscribers
|
|
6
|
+
# A notification subscriber of the gem, switched by an option of the
|
|
7
|
+
# configuration: installed once (Audit.install!), silenced while the
|
|
8
|
+
# option is off. ActiveSupport asks #silenced? before it delivers, so a
|
|
9
|
+
# silenced subscriber costs nothing, and the option can change at run
|
|
10
|
+
# time. The option's writer calls Gate.changed!, which is how Audit::Gate
|
|
11
|
+
# knows the answer without asking every time.
|
|
12
|
+
#
|
|
13
|
+
# Subclasses name what they subscribe to (#pattern), when they are
|
|
14
|
+
# silenced, and what they do with an event's payload (#deliver).
|
|
15
|
+
class Base
|
|
16
|
+
# @return [String, Regexp] the notification(s) to subscribe to
|
|
17
|
+
def pattern
|
|
18
|
+
raise NotImplementedError
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @param _name [String] the notification
|
|
22
|
+
# @return [Boolean]
|
|
23
|
+
def silenced?(_name)
|
|
24
|
+
raise NotImplementedError
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Whether it can be handed the light payload ({ data:, objects: }) in
|
|
28
|
+
# place of a built event (Audit::Gate#direct_listeners).
|
|
29
|
+
def light? = false
|
|
30
|
+
|
|
31
|
+
# The light payload, from Audit.instrument (the hook path that builds
|
|
32
|
+
# no event).
|
|
33
|
+
def deliver_light(name, payload)
|
|
34
|
+
deliver(name, payload)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @return [Object] the ActiveSupport subscriber, for Subscribers.uninstall!
|
|
38
|
+
def subscribe!
|
|
39
|
+
ActiveSupport::Notifications.subscribe(pattern, self)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# ActiveSupport's timed subscriber interface.
|
|
43
|
+
def call(name, _started, _finished, _id, payload)
|
|
44
|
+
deliver(name, payload)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
# @param name [String] "<event>.permission_forge"
|
|
50
|
+
# @param payload [Hash] see Audit::Recorder#notification_payload
|
|
51
|
+
def deliver(name, payload)
|
|
52
|
+
raise NotImplementedError
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def configuration
|
|
56
|
+
PermissionForge.configuration
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# A hook that raises follows config.audit_hook_failure_strategy: it
|
|
60
|
+
# never breaks the request unless the strategy is :raise.
|
|
61
|
+
def guarded(event, hook)
|
|
62
|
+
yield
|
|
63
|
+
rescue StandardError => e
|
|
64
|
+
Failure.handle(e, strategy: configuration.audit_hook_failure_strategy, operation: :hook, event: event, hook: hook)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Audit
|
|
5
|
+
module Subscribers
|
|
6
|
+
# The two hooks that existed before the audit event log, kept with the
|
|
7
|
+
# call signatures they always had, as subscribers of the runtime events
|
|
8
|
+
# that replaced them. Installed always and silenced while the hook is
|
|
9
|
+
# nil, so a hook set at run time is called from then on.
|
|
10
|
+
#
|
|
11
|
+
# Alone (no other listener, the group not stored) they are handed the
|
|
12
|
+
# decision without an event being built (Audit::Gate#direct_listeners):
|
|
13
|
+
# a hook costs what it cost before the audit event log, roughly.
|
|
14
|
+
#
|
|
15
|
+
# A hook that raises no longer only logs: the failure follows
|
|
16
|
+
# config.audit_hook_failure_strategy (:report by default — logged and
|
|
17
|
+
# handed to Rails.error; :raise raises it).
|
|
18
|
+
module LegacyHooks
|
|
19
|
+
# config.on_rule_bypass(user:, resource:, reason: :view_all_or_modify_all)
|
|
20
|
+
# — called when view_all or modify_all lets `find_by` / `find_by!` /
|
|
21
|
+
# `permission_scoped` skip the row-level rules, as before. Not called
|
|
22
|
+
# for auto-scoping (it would fire on every query) nor for the other
|
|
23
|
+
# bypass reasons, which only rule.bypassed reports.
|
|
24
|
+
class RuleBypass < Base
|
|
25
|
+
REASONS = %w[view_all modify_all].freeze
|
|
26
|
+
VIA = %w[find_by permission_scoped].freeze
|
|
27
|
+
LEGACY_REASON = :view_all_or_modify_all
|
|
28
|
+
|
|
29
|
+
def pattern = 'rule.bypassed.permission_forge'
|
|
30
|
+
|
|
31
|
+
def light? = true
|
|
32
|
+
|
|
33
|
+
def silenced?(_name)
|
|
34
|
+
configuration.on_rule_bypass.nil?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def deliver(_name, payload)
|
|
40
|
+
hook = configuration.on_rule_bypass
|
|
41
|
+
data = payload[:data]
|
|
42
|
+
return unless hook && REASONS.include?(data['reason']) && VIA.include?(data['via'])
|
|
43
|
+
|
|
44
|
+
guarded('rule.bypassed', :on_rule_bypass) do
|
|
45
|
+
hook.call(user: payload[:objects][:user], resource: data['model'], reason: LEGACY_REASON)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# config.on_field_filtered(resource_name:, field_name:, action:, user:)
|
|
51
|
+
# — called once for every field decision, in the order the decisions
|
|
52
|
+
# were made, as before: one field.access event (a record read)
|
|
53
|
+
# carries all of them in payload[:objects][:decisions]. Each call is
|
|
54
|
+
# guarded on its own, so one that raises does not stop the others.
|
|
55
|
+
class FieldFiltered < Base
|
|
56
|
+
def pattern = 'field.access.permission_forge'
|
|
57
|
+
|
|
58
|
+
def light? = true
|
|
59
|
+
|
|
60
|
+
def silenced?(_name)
|
|
61
|
+
configuration.on_field_filtered.nil?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# One decision, as it is made (Audit::FieldAccess::Direct).
|
|
65
|
+
def deliver_decision(resource_name, field_name, action, user)
|
|
66
|
+
hook = configuration.on_field_filtered
|
|
67
|
+
return unless hook
|
|
68
|
+
|
|
69
|
+
resource_name = resource_name.is_a?(Symbol) ? resource_name.name : resource_name.to_s
|
|
70
|
+
guarded('field.access', :on_field_filtered) do
|
|
71
|
+
hook.call(resource_name: resource_name, field_name: field_name, action: action, user: user)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def deliver(_name, payload)
|
|
78
|
+
hook = configuration.on_field_filtered
|
|
79
|
+
return unless hook
|
|
80
|
+
|
|
81
|
+
Array(payload[:objects][:decisions]).each do |decision|
|
|
82
|
+
next unless decision.hook
|
|
83
|
+
|
|
84
|
+
guarded('field.access', :on_field_filtered) do
|
|
85
|
+
hook.call(resource_name: decision.resource_name, field_name: decision.field_name,
|
|
86
|
+
action: decision.action, user: decision.user)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
register(:on_rule_bypass, ->(_config) { LegacyHooks::RuleBypass.new.subscribe! })
|
|
94
|
+
register(:on_field_filtered, ->(_config) { LegacyHooks::FieldFiltered.new.subscribe! })
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Audit
|
|
5
|
+
module Subscribers
|
|
6
|
+
# config.audit_log_subscriber = true: one structured line per audit
|
|
7
|
+
# event (stored or not) in Rails.logger, at info level —
|
|
8
|
+
#
|
|
9
|
+
# [PermissionForge] audit {"event":"access.denied","category":"access","actor_type":"user","actor_id":"7",...}
|
|
10
|
+
#
|
|
11
|
+
# The line carries ids, never the name snapshots (actor_label,
|
|
12
|
+
# record_label): logs usually travel further than the audit table.
|
|
13
|
+
# Silenced while the option is off; notifications that are not audit
|
|
14
|
+
# events (cache_failure.permission_forge) are left alone.
|
|
15
|
+
class Log < Base
|
|
16
|
+
SUFFIX = '.permission_forge'
|
|
17
|
+
PREFIX = '[PermissionForge] audit '
|
|
18
|
+
OMITTED = %i[actor_label record_label audit_event objects].freeze
|
|
19
|
+
|
|
20
|
+
def pattern = /\.permission_forge\z/
|
|
21
|
+
|
|
22
|
+
def silenced?(_name)
|
|
23
|
+
!configuration.audit_log_subscriber
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def deliver(name, payload)
|
|
29
|
+
return unless configuration.audit_log_subscriber && defined?(Rails) && Rails.logger
|
|
30
|
+
|
|
31
|
+
event = name.delete_suffix(SUFFIX)
|
|
32
|
+
return unless EventCatalog.registered?(event)
|
|
33
|
+
|
|
34
|
+
guarded(event, :audit_log_subscriber) { Rails.logger.info(PREFIX + line(payload).to_json) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def line(payload)
|
|
38
|
+
payload.except(*OMITTED).compact
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
register(:log, ->(_config) { Log.new.subscribe! })
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|