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,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Audit
|
|
5
|
+
# Whether a runtime event could be stored or heard right now — asked on
|
|
6
|
+
# the hottest paths of the gem (every attribute read under
|
|
7
|
+
# MaskedAttributes asks it for field.access), so the answer is kept and
|
|
8
|
+
# only worked out again when something it depends on changed. The
|
|
9
|
+
# generation is bumped by:
|
|
10
|
+
#
|
|
11
|
+
# - the writers of the options that decide it (audit_runtime_events,
|
|
12
|
+
# on_field_filtered, on_rule_bypass, audit_log_subscriber);
|
|
13
|
+
# - every subscribe and unsubscribe: they go through
|
|
14
|
+
# ActiveSupport::Notifications::Fanout#clear_cache (FanoutHook), also
|
|
15
|
+
# for a pattern subscriber;
|
|
16
|
+
# - a new notifier (ActiveSupport::Notifications.notifier=, NotifierHook);
|
|
17
|
+
# - a new Configuration (PermissionForge.reset!, Configuration.new). A
|
|
18
|
+
# configuration object put back in place past PermissionForge's API
|
|
19
|
+
# (instance_variable_set with one made earlier) is not noticed.
|
|
20
|
+
#
|
|
21
|
+
# The answer is a superset: a subscriber of the host that says it is
|
|
22
|
+
# silenced still counts (it may change its mind without telling), and
|
|
23
|
+
# Audit.instrument asks the notifier again before it builds anything. The
|
|
24
|
+
# gem's own subscribers (Subscribers::Base) are silenced by their option,
|
|
25
|
+
# whose writer bumps the generation, so they count only while it is set.
|
|
26
|
+
# Where the notifier is not a Fanout with the hook, nothing is kept and
|
|
27
|
+
# the notifier is asked every time.
|
|
28
|
+
#
|
|
29
|
+
# Gate.for('field.access').open? # => false
|
|
30
|
+
#
|
|
31
|
+
# @api private
|
|
32
|
+
class Gate
|
|
33
|
+
# Bumps the generation whenever ActiveSupport changes who listens.
|
|
34
|
+
module FanoutHook
|
|
35
|
+
def clear_cache(...)
|
|
36
|
+
super
|
|
37
|
+
ensure
|
|
38
|
+
PermissionForge::Audit::Gate.changed!
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Bumps the generation when the notifier is replaced.
|
|
43
|
+
module NotifierHook
|
|
44
|
+
def notifier=(notifier)
|
|
45
|
+
super
|
|
46
|
+
ensure
|
|
47
|
+
PermissionForge::Audit::Gate.changed!
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
MUTEX = Mutex.new
|
|
52
|
+
# The generation, read on every check: an Array element in a constant
|
|
53
|
+
# is the cheapest read Ruby has.
|
|
54
|
+
GENERATION = [0] # rubocop:disable Style/MutableConstant -- bumped in place by changed!
|
|
55
|
+
|
|
56
|
+
@gates = {}
|
|
57
|
+
|
|
58
|
+
class << self
|
|
59
|
+
# @return [Integer] bumped by every change the answers depend on
|
|
60
|
+
def generation = GENERATION[0]
|
|
61
|
+
|
|
62
|
+
# @param event [String] an access event of the catalogue
|
|
63
|
+
# @return [Gate]
|
|
64
|
+
def for(event)
|
|
65
|
+
@gates[event] || MUTEX.synchronize { @gates[event.to_s] ||= new(EventCatalog.fetch(event).name) }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Something an answer depends on changed: every gate works it out again.
|
|
69
|
+
def changed!
|
|
70
|
+
MUTEX.synchronize { GENERATION[0] += 1 }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Hooks the generation to ActiveSupport's subscriptions and notifier
|
|
74
|
+
# (once; Audit.install!). Without the hooks nothing is kept.
|
|
75
|
+
def install!
|
|
76
|
+
fanout = ActiveSupport::Notifications::Fanout
|
|
77
|
+
fanout.prepend(FanoutHook) if fanout.method_defined?(:clear_cache) && !(fanout <= FanoutHook)
|
|
78
|
+
notifications = ActiveSupport::Notifications.singleton_class
|
|
79
|
+
notifications.prepend(NotifierHook) unless notifications <= NotifierHook
|
|
80
|
+
changed!
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Whether answers can be kept for this notifier.
|
|
84
|
+
def tracking?(notifier)
|
|
85
|
+
notifier.is_a?(FanoutHook) && ActiveSupport::Notifications.singleton_class <= NotifierHook
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# @param name [String] a registered access event
|
|
90
|
+
def initialize(name)
|
|
91
|
+
@name = name
|
|
92
|
+
@snapshot = nil
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# The kept answer is one frozen Array [generation, answer], replaced
|
|
96
|
+
# whole: a reader on another thread sees an old answer or a new one,
|
|
97
|
+
# never half of each.
|
|
98
|
+
#
|
|
99
|
+
# @return [Boolean] whether the event's group is stored or anyone may listen
|
|
100
|
+
def open?
|
|
101
|
+
snapshot = @snapshot
|
|
102
|
+
return snapshot[1] if snapshot && snapshot[0] == GENERATION[0]
|
|
103
|
+
|
|
104
|
+
refresh[1]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# The gem's own subscribers that take a light payload, when they are
|
|
108
|
+
# the only ones that hear the event and its group is not stored: the
|
|
109
|
+
# legacy hooks alone. Audit.instrument then hands them the payload
|
|
110
|
+
# without building an event (no Recorder, no notification) — a hook set
|
|
111
|
+
# for years must not cost what a full audit event costs. nil otherwise,
|
|
112
|
+
# and wherever nothing is kept (no tracking).
|
|
113
|
+
#
|
|
114
|
+
# @return [Array<Subscribers::Base>, nil]
|
|
115
|
+
def direct_listeners
|
|
116
|
+
snapshot = @snapshot
|
|
117
|
+
return snapshot[2] if snapshot && snapshot[0] == GENERATION[0]
|
|
118
|
+
|
|
119
|
+
refresh[2]
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
private
|
|
123
|
+
|
|
124
|
+
# The generation is read before the answer is worked out: a change made
|
|
125
|
+
# meanwhile leaves a snapshot that is already out of date.
|
|
126
|
+
#
|
|
127
|
+
# @return [Array(Integer, Boolean, Array|nil)]
|
|
128
|
+
def refresh
|
|
129
|
+
generation = GENERATION[0]
|
|
130
|
+
notifier = ActiveSupport::Notifications.notifier
|
|
131
|
+
entry = EventCatalog.fetch(@name)
|
|
132
|
+
stored = PermissionForge.configuration.store_runtime_group?(entry.runtime_group)
|
|
133
|
+
listeners = heard_by(notifier, entry.notification)
|
|
134
|
+
tracking = Gate.tracking?(notifier)
|
|
135
|
+
snapshot = [generation, stored || !listeners.empty?, (direct_of(listeners) if tracking && !stored)].freeze
|
|
136
|
+
@snapshot = (snapshot if tracking)
|
|
137
|
+
snapshot
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# What hears the notification: a delegate per listener (the listener
|
|
141
|
+
# itself when it has none); [:unknown] when the notifier cannot say.
|
|
142
|
+
def heard_by(notifier, notification)
|
|
143
|
+
unless notifier.respond_to?(:all_listeners_for)
|
|
144
|
+
return notifier.listening?(notification) ? [:unknown] : []
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
notifier.all_listeners_for(notification).filter_map do |listener|
|
|
148
|
+
delegate = listener.respond_to?(:delegate) ? listener.delegate : listener
|
|
149
|
+
delegate if counts?(delegate, notification)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def counts?(delegate, notification)
|
|
154
|
+
return true unless delegate.is_a?(Subscribers::Base)
|
|
155
|
+
|
|
156
|
+
!delegate.silenced?(notification)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def direct_of(listeners)
|
|
160
|
+
return if listeners.empty?
|
|
161
|
+
return unless listeners.all? { |delegate| delegate.is_a?(Subscribers::Base) && delegate.light? }
|
|
162
|
+
|
|
163
|
+
listeners.freeze
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Audit
|
|
5
|
+
# Every ActiveJob job runs with source 'job' in the audit context. The
|
|
6
|
+
# actor stays what the context already says (a job performed inline
|
|
7
|
+
# inside an admin request keeps the admin) or what the job names with
|
|
8
|
+
# PermissionForge.with_audit_actor; otherwise the usual resolution
|
|
9
|
+
# (PermissionForge.current_user, else unknown).
|
|
10
|
+
#
|
|
11
|
+
# Installed by the engine through ActiveSupport.on_load(:active_job), so
|
|
12
|
+
# an app without ActiveJob never loads it.
|
|
13
|
+
#
|
|
14
|
+
# @api private
|
|
15
|
+
module JobContext
|
|
16
|
+
def self.install!
|
|
17
|
+
ActiveSupport.on_load(:active_job) { include PermissionForge::Audit::JobContext::Hook }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Included into ActiveJob::Base.
|
|
21
|
+
module Hook
|
|
22
|
+
extend ActiveSupport::Concern
|
|
23
|
+
|
|
24
|
+
included do
|
|
25
|
+
around_perform do |_job, block|
|
|
26
|
+
PermissionForge::Audit::Context.with(source: 'job') { block.call }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Audit
|
|
5
|
+
# The one answer to "may this request read the audit log?", asked by the
|
|
6
|
+
# JSON API endpoint and by the HTML pages and exports alike.
|
|
7
|
+
#
|
|
8
|
+
# The request has already passed the surface's own gate (api_enabled and
|
|
9
|
+
# api_auth for the API, the admin authentication for the HTML pages) by the
|
|
10
|
+
# time this is asked, so with `config.audit_log_authorization` unset the
|
|
11
|
+
# answer is yes: reading the log needs no more than the admin access that
|
|
12
|
+
# reading every other admin endpoint needs.
|
|
13
|
+
#
|
|
14
|
+
# With a lambda(controller) set, only an exact `true` allows. Anything else
|
|
15
|
+
# (false, nil, a truthy non-true value) denies, and so does a lambda that
|
|
16
|
+
# raises: an authorization hook that fails must never open the log.
|
|
17
|
+
#
|
|
18
|
+
# @api private
|
|
19
|
+
module LogAccess
|
|
20
|
+
class << self
|
|
21
|
+
# @param controller [ActionController::Base] the request's controller
|
|
22
|
+
# @param configuration [PermissionForge::Configuration]
|
|
23
|
+
# @return [Boolean]
|
|
24
|
+
def allowed?(controller, configuration: PermissionForge.configuration)
|
|
25
|
+
hook = configuration.audit_log_authorization
|
|
26
|
+
return true if hook.nil?
|
|
27
|
+
|
|
28
|
+
true.equal?(hook.call(controller))
|
|
29
|
+
rescue StandardError => e
|
|
30
|
+
Failure.handle(e, strategy: :report, operation: :authorize, hook: 'audit_log_authorization')
|
|
31
|
+
false
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Audit
|
|
5
|
+
# Builds the row of one event from its catalogue entry, the audit context
|
|
6
|
+
# and the caller's payload, stores it (AuditEvent) and gives the
|
|
7
|
+
# notification payload. The row is built once (#attributes) and kept.
|
|
8
|
+
#
|
|
9
|
+
# Payload keys (anything else raises — a typo must not pass silently):
|
|
10
|
+
# record: the record the event is about. A permission model
|
|
11
|
+
# (Concerns::RecordsAuditEvents) fills its columns
|
|
12
|
+
# through #audit_reference. Any other record fills
|
|
13
|
+
# record_type (its base class), record_id and, when
|
|
14
|
+
# the table has a uuid column, record_uuid from the
|
|
15
|
+
# raw column — never through its readers, which a
|
|
16
|
+
# masked host model guards, and never a label
|
|
17
|
+
# record_label: the label of a host record, if any
|
|
18
|
+
# data: event-specific Hash, stored as JSON with string keys
|
|
19
|
+
# reason: free text or ticket reference
|
|
20
|
+
# actor: an Actor, a user or a Hash — overrides Actor.current
|
|
21
|
+
# source: overrides Source.current
|
|
22
|
+
# subject_user_id: the person whose access is affected
|
|
23
|
+
# permission_set: a PermissionSet (fills id and uuid) or an id
|
|
24
|
+
# occurred_at: defaults to now (UTC)
|
|
25
|
+
# record_type, record_id, record_uuid, permission_set_id,
|
|
26
|
+
# permission_set_uuid, tenant_id: column overrides
|
|
27
|
+
# ambient_tenant_id: the tenant that was ambient when the event
|
|
28
|
+
# happened, for an event written later
|
|
29
|
+
# (Audit::Detached); defaults to the one ambient now
|
|
30
|
+
# objects: live objects for the subscribers in this process
|
|
31
|
+
# (the user a decision was made for, …): handed over
|
|
32
|
+
# as payload[:objects], never stored or serialized
|
|
33
|
+
#
|
|
34
|
+
# @api private
|
|
35
|
+
class Recorder
|
|
36
|
+
OPTIONS = %i[record data reason actor source subject_user_id permission_set occurred_at
|
|
37
|
+
record_type record_id record_uuid record_label permission_set_id permission_set_uuid tenant_id
|
|
38
|
+
ambient_tenant_id objects].freeze
|
|
39
|
+
NO_OBJECTS = {}.freeze
|
|
40
|
+
REFERENCE_COLUMNS = %i[record_type record_id record_uuid record_label permission_set_id permission_set_uuid
|
|
41
|
+
subject_user_id tenant_id].freeze
|
|
42
|
+
|
|
43
|
+
attr_reader :entry
|
|
44
|
+
|
|
45
|
+
# @param entry [EventCatalog::Entry]
|
|
46
|
+
# @param payload [Hash] see OPTIONS
|
|
47
|
+
def initialize(entry, payload)
|
|
48
|
+
unknown = payload.keys - OPTIONS
|
|
49
|
+
raise ArgumentError, "unknown audit payload key(s) #{unknown.map(&:inspect).join(', ')} (known: #{OPTIONS.join(', ')})" \
|
|
50
|
+
unless unknown.empty?
|
|
51
|
+
|
|
52
|
+
@entry = entry
|
|
53
|
+
@payload = payload
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def name
|
|
57
|
+
entry.name
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# The event as a Hash: every column of AuditEvent (uuid included) plus
|
|
61
|
+
# tenant_id. What subscribers receive, with stored: and audit_event:.
|
|
62
|
+
# Every string column is cleaned (Audit::ColumnValues).
|
|
63
|
+
def attributes
|
|
64
|
+
@attributes ||= ColumnValues.clean(raw_attributes).freeze
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @return [AuditEvent] the stored event
|
|
68
|
+
def store!
|
|
69
|
+
PermissionForge::AuditEvent.store!(attributes.except(:tenant_id).merge(subject_tenant_id: attributes[:tenant_id]))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# @param stored [AuditEvent, nil]
|
|
73
|
+
# @param deferred [Boolean] the row is written later: a runtime event
|
|
74
|
+
# heard inside an open transaction is stored once the outermost
|
|
75
|
+
# transaction has ended (Audit.instrument)
|
|
76
|
+
def notification_payload(stored, deferred: false)
|
|
77
|
+
attributes.merge(stored: !stored.nil?, audit_event: stored, deferred: deferred,
|
|
78
|
+
objects: @payload[:objects] || NO_OBJECTS)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def raw_attributes
|
|
84
|
+
{
|
|
85
|
+
uuid: Audit.new_id, event: entry.name, category: entry.category,
|
|
86
|
+
**actor.to_columns, source: source,
|
|
87
|
+
**reference,
|
|
88
|
+
data: data, reason: reason,
|
|
89
|
+
request_id: Context[:request_id], ip: Context[:ip],
|
|
90
|
+
correlation_id: correlation_id, occurred_at: occurred_at
|
|
91
|
+
}
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def actor
|
|
95
|
+
Actor.coerce(@payload[:actor]) || Actor.current
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def source
|
|
99
|
+
@payload[:source] ? Source.validate!(@payload[:source]) : Source.current
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def reason
|
|
103
|
+
(@payload[:reason] || Context[:reason])&.to_s
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def data
|
|
107
|
+
(@payload[:data] || {}).as_json
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def occurred_at
|
|
111
|
+
(@payload[:occurred_at] || Time.current).utc
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# The context's id, else one per open transaction, else one of its own:
|
|
115
|
+
# a correlation id is never blank.
|
|
116
|
+
def correlation_id
|
|
117
|
+
Context[:correlation_id] || Context.transaction_correlation_id || Audit.new_id
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Columns from the record, then from permission_set:, then explicit overrides.
|
|
121
|
+
def reference
|
|
122
|
+
columns = REFERENCE_COLUMNS.index_with { nil }.merge!(record_reference, permission_set_reference,
|
|
123
|
+
@payload.slice(*REFERENCE_COLUMNS))
|
|
124
|
+
columns[:record_id] = columns[:record_id]&.to_s
|
|
125
|
+
columns[:subject_user_id] = columns[:subject_user_id]&.to_s
|
|
126
|
+
columns[:tenant_id] = ambient_tenant_id if columns[:tenant_id].nil?
|
|
127
|
+
columns
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Pinned when the event is built: a row written later (after the
|
|
131
|
+
# transaction it was noticed in has ended) keeps the tenant of the
|
|
132
|
+
# moment it happened, whatever is ambient at the write.
|
|
133
|
+
def ambient_tenant_id
|
|
134
|
+
@payload.key?(:ambient_tenant_id) ? @payload[:ambient_tenant_id] : PermissionForge.ambient_tenant_id
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def record_reference
|
|
138
|
+
record = @payload[:record]
|
|
139
|
+
return {} unless record
|
|
140
|
+
return record.audit_reference if record.respond_to?(:audit_reference)
|
|
141
|
+
|
|
142
|
+
{ record_type: record_type_of(record), record_id: record.id, record_uuid: raw_uuid_of(record) }
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def record_type_of(record)
|
|
146
|
+
klass = record.class
|
|
147
|
+
klass.respond_to?(:base_class) ? klass.base_class.name : klass.name
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# The uuid column as stored, past the record's readers (a host model
|
|
151
|
+
# with MaskedAttributes guards those, and a guard that audits reads
|
|
152
|
+
# would recurse).
|
|
153
|
+
def raw_uuid_of(record)
|
|
154
|
+
return unless record.respond_to?(:has_attribute?) && record.has_attribute?('uuid')
|
|
155
|
+
|
|
156
|
+
record.read_attribute_before_type_cast('uuid')
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def permission_set_reference
|
|
160
|
+
set = @payload[:permission_set]
|
|
161
|
+
return {} unless set
|
|
162
|
+
return { permission_set_id: set.id, permission_set_uuid: set.uuid } if set.respond_to?(:uuid)
|
|
163
|
+
|
|
164
|
+
{ permission_set_id: set }
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Audit
|
|
5
|
+
# The payloads of refusal events built from what a client sent, bounded:
|
|
6
|
+
# a refused request must not be able to make its own record too big to
|
|
7
|
+
# store (MySQL refuses an over-long value; a huge JSON row costs the
|
|
8
|
+
# store), nor shape it with nested parameters.
|
|
9
|
+
#
|
|
10
|
+
# @api private
|
|
11
|
+
module Refusals
|
|
12
|
+
# How many of the people concerned are listed; user_count has them all.
|
|
13
|
+
USER_IDS_LIMIT = 100
|
|
14
|
+
VALUE_LIMIT = 255
|
|
15
|
+
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
# The payload of a refused change to who holds a Permission Set or
|
|
19
|
+
# belongs to a Permission Set Group, for Audit.record / pf_audit_refused.
|
|
20
|
+
#
|
|
21
|
+
# @param target [PermissionForge::PermissionSet, PermissionForge::PermissionSetGroup, nil]
|
|
22
|
+
# @param user_ids [Object] as the client sent them: a scalar, an Array, anything
|
|
23
|
+
# @param action [Symbol, String, nil] e.g. :grant, :revoke, :add_member
|
|
24
|
+
# @param hook [String] the option that refused (data 'hook')
|
|
25
|
+
# @return [Hash] subject_user_id: (one person only), permission_set: or record:, data:
|
|
26
|
+
def assignment(target, user_ids, hook:, action: nil)
|
|
27
|
+
ids = user_ids_of(user_ids)
|
|
28
|
+
data = { 'reason' => 'assignment_not_authorized', 'hook' => hook,
|
|
29
|
+
'user_ids' => ids.first(USER_IDS_LIMIT), 'user_count' => ids.size }
|
|
30
|
+
data['action'] = action.to_s if action
|
|
31
|
+
{ subject_user_id: (ids.first if ids.one?), data: data, **target_reference(target) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Scalars only (strings, numbers), each as a String of at most
|
|
35
|
+
# VALUE_LIMIT characters; nested arrays are flattened, hashes dropped.
|
|
36
|
+
#
|
|
37
|
+
# @return [Array<String>]
|
|
38
|
+
def user_ids_of(user_ids)
|
|
39
|
+
return [] if user_ids.nil? || user_ids.is_a?(Hash) || user_ids.respond_to?(:to_unsafe_h)
|
|
40
|
+
|
|
41
|
+
Array(user_ids).flatten.filter_map { |id| scalar(id) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @return [String, nil] a non-empty String or number, capped
|
|
45
|
+
def scalar(value)
|
|
46
|
+
return unless value.is_a?(String) || value.is_a?(Numeric)
|
|
47
|
+
|
|
48
|
+
value.to_s[0, VALUE_LIMIT].presence
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# A Permission Set fills permission_set_id/uuid; a group is the record.
|
|
52
|
+
def target_reference(target)
|
|
53
|
+
case target
|
|
54
|
+
when nil then {}
|
|
55
|
+
when PermissionForge::PermissionSet then { permission_set: target }
|
|
56
|
+
else { record: target }
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Audit
|
|
5
|
+
# The audit context of one admin UI or API request, set by the
|
|
6
|
+
# around-action of both base controllers:
|
|
7
|
+
#
|
|
8
|
+
# around_action :pf_audit_request
|
|
9
|
+
# def pf_audit_request(&) = PermissionForge::Audit::RequestContext.wrap(self, source: 'admin_ui', &)
|
|
10
|
+
#
|
|
11
|
+
# Inside the block every event carries the actor (Audit::ActorResolver),
|
|
12
|
+
# the source, the request id and IP (config.audit_request_context, else
|
|
13
|
+
# `request.request_id` and `request.remote_ip`) and, for a request that
|
|
14
|
+
# can change something (anything but GET and HEAD), the `reason`
|
|
15
|
+
# parameter and one correlation id for all its events. A read-only page
|
|
16
|
+
# in Audit::AuditedViews is recorded after a successful response.
|
|
17
|
+
#
|
|
18
|
+
# Nothing is set outside the block, and the block restores the previous
|
|
19
|
+
# context also when the action raises.
|
|
20
|
+
#
|
|
21
|
+
# @api private
|
|
22
|
+
class RequestContext
|
|
23
|
+
# The reason column is a varchar(255) (MySQL counts characters).
|
|
24
|
+
REASON_LIMIT = 255
|
|
25
|
+
REQUEST_VALUE_LIMIT = 255
|
|
26
|
+
READ_ONLY_METHODS = %w[GET HEAD].freeze
|
|
27
|
+
|
|
28
|
+
class << self
|
|
29
|
+
# @param controller [ActionController::Metal]
|
|
30
|
+
# @param source [String] 'admin_ui' or 'api'
|
|
31
|
+
# @return the value of the block
|
|
32
|
+
def wrap(controller, source:, &)
|
|
33
|
+
new(controller, source).wrap(&)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The request id and IP to store: config.audit_request_context
|
|
37
|
+
# (lambda(controller) → { request_id:, ip: }; a key it returns — also
|
|
38
|
+
# as nil — replaces the default) over request.request_id and the
|
|
39
|
+
# remote IP (#remote_ip). A lambda that raises is reported and the
|
|
40
|
+
# defaults are used.
|
|
41
|
+
#
|
|
42
|
+
# @return [Hash] { request_id:, ip: }
|
|
43
|
+
def request_slots(controller, configuration: PermissionForge.configuration)
|
|
44
|
+
request = controller.request
|
|
45
|
+
slots = { request_id: request.request_id, ip: remote_ip(request).first }
|
|
46
|
+
slots.merge!(custom_request_slots(controller, configuration))
|
|
47
|
+
slots.transform_values { |value| value.presence && value.to_s[0, REQUEST_VALUE_LIMIT] }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# `request.remote_ip`, which raises IpSpoofAttackError when the
|
|
51
|
+
# Client-IP and X-Forwarded-For headers disagree. That must neither
|
|
52
|
+
# fail the request nor keep a refused sign-in out of the log: the
|
|
53
|
+
# peer address is used instead, and the second value says so.
|
|
54
|
+
#
|
|
55
|
+
# @return [Array(String, Boolean)] [ip, spoofing suspected?]
|
|
56
|
+
def remote_ip(request)
|
|
57
|
+
[request.remote_ip, false]
|
|
58
|
+
rescue ActionDispatch::RemoteIp::IpSpoofAttackError
|
|
59
|
+
[request.remote_addr, true]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# The `reason` parameter (form field or JSON body key) of a request
|
|
63
|
+
# that can change something: a String cleaned as an event column is
|
|
64
|
+
# (one line, no NUL or other control character — PostgreSQL refuses a
|
|
65
|
+
# NUL, and the assignment rows keep the reason too), stripped, nil
|
|
66
|
+
# when blank, capped at REASON_LIMIT characters.
|
|
67
|
+
#
|
|
68
|
+
# @return [String, nil]
|
|
69
|
+
def reason(controller)
|
|
70
|
+
return unless mutating?(controller.request)
|
|
71
|
+
|
|
72
|
+
value = controller.params[:reason]
|
|
73
|
+
ColumnValues.value(value.strip)[0, REASON_LIMIT].strip.presence if value.is_a?(String)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def mutating?(request)
|
|
77
|
+
!READ_ONLY_METHODS.include?(request.request_method)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Records a refusal — a failed sign-in, a denied authorization, a
|
|
81
|
+
# locked record — through Audit::Detached, so that a transaction the
|
|
82
|
+
# caller rolls back does not take it along. Works before the request's
|
|
83
|
+
# context is set (authentication runs first): source, request id and
|
|
84
|
+
# IP are set for the event, and the actor is `actor:`, else the
|
|
85
|
+
# request's, else whoever the host names (ActorResolver, no fallback:
|
|
86
|
+
# an unidentified client stays unknown). `data` gets the request's
|
|
87
|
+
# method, path and endpoint, and 'ip_spoof_suspected' => true when
|
|
88
|
+
# the forwarding headers contradict each other (#remote_ip).
|
|
89
|
+
#
|
|
90
|
+
# @param event [String] e.g. 'admin.auth_failed', 'api.auth_failed', 'admin.lock_refused'
|
|
91
|
+
# @param payload [Hash] see Audit::Recorder
|
|
92
|
+
# @return [AuditEvent, nil]
|
|
93
|
+
def refused(controller, event, source:, actor: nil, **payload)
|
|
94
|
+
data = request_data(controller).merge(payload.delete(:data) || {})
|
|
95
|
+
data['ip_spoof_suspected'] = true if remote_ip(controller.request).last
|
|
96
|
+
Context.with(source: source, **request_slots(controller)) do
|
|
97
|
+
actor ||= Context[:actor] || ActorResolver.call(controller, source: source, fallback: false)
|
|
98
|
+
Detached.record(event, actor: actor, source: source, data: data, **payload)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# @return [Hash] 'method', 'path' and 'endpoint' ("controller_path#action")
|
|
103
|
+
def request_data(controller)
|
|
104
|
+
request = controller.request
|
|
105
|
+
{ 'method' => request.request_method, 'path' => request.path,
|
|
106
|
+
'endpoint' => "#{controller.controller_path}##{controller.action_name}" }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
def custom_request_slots(controller, configuration)
|
|
112
|
+
custom = configuration.audit_request_context
|
|
113
|
+
return {} unless custom
|
|
114
|
+
|
|
115
|
+
answer = custom.call(controller)
|
|
116
|
+
answer.respond_to?(:to_h) ? answer.to_h.symbolize_keys.slice(:request_id, :ip) : {}
|
|
117
|
+
rescue StandardError => e
|
|
118
|
+
Failure.handle(e, strategy: configuration.audit_hook_failure_strategy, operation: :request_context)
|
|
119
|
+
{}
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def initialize(controller, source)
|
|
124
|
+
@controller = controller
|
|
125
|
+
@source = source
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# @return the value of the block
|
|
129
|
+
def wrap(&block)
|
|
130
|
+
Context.with(**slots) do
|
|
131
|
+
result = self.class.mutating?(@controller.request) ? Audit.correlate(&block) : block.call
|
|
132
|
+
AuditedViews.record(@controller)
|
|
133
|
+
result
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
private
|
|
138
|
+
|
|
139
|
+
# An actor or source the host has already named around the request
|
|
140
|
+
# (PermissionForge.with_audit_actor in its own around-action) wins, as
|
|
141
|
+
# an explicit block does everywhere else.
|
|
142
|
+
def slots
|
|
143
|
+
{
|
|
144
|
+
actor: Context[:actor] || ActorResolver.call(@controller, source: @source),
|
|
145
|
+
source: Context[:source] || @source,
|
|
146
|
+
reason: self.class.reason(@controller),
|
|
147
|
+
**self.class.request_slots(@controller)
|
|
148
|
+
}
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|