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,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
# What the audit event page (audit_events/show) lays out for one event: the
|
|
6
|
+
# event's data split into its before/after changes, its snapshot and
|
|
7
|
+
# whatever else it carries, and which of the who/what rows have a value.
|
|
8
|
+
# The template only draws it.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# detail = AuditEventDetail.new(event, AuditLookups.new([event]))
|
|
12
|
+
# detail.change_rows # => [['mask_strategy', 'full', 'last4']]
|
|
13
|
+
# detail.no_data? # => false
|
|
14
|
+
class AuditEventDetail
|
|
15
|
+
SNAPSHOT_ENDINGS = %w[destroyed revoked removed].freeze
|
|
16
|
+
|
|
17
|
+
attr_reader :event, :lookups
|
|
18
|
+
|
|
19
|
+
def initialize(event, lookups)
|
|
20
|
+
@event = event
|
|
21
|
+
@lookups = lookups
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The person who acted, when the actor is a user we can open.
|
|
25
|
+
def actor_user
|
|
26
|
+
lookups.person(event.actor_id) if event.actor_type == 'user'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def person = lookups.person(event.subject_user_id)
|
|
30
|
+
def permission_set = lookups.permission_set(event.permission_set_id)
|
|
31
|
+
|
|
32
|
+
def actor_id? = event.actor_id.present?
|
|
33
|
+
def record? = event.record_type.present?
|
|
34
|
+
def record_id? = event.record_id.present?
|
|
35
|
+
def record_uuid? = event.record_uuid.present?
|
|
36
|
+
def permission_set? = event.permission_set_id.present? || event.permission_set_uuid.present?
|
|
37
|
+
def permission_set_key = event.permission_set_id || event.permission_set_uuid
|
|
38
|
+
def subject_user? = event.subject_user_id.present?
|
|
39
|
+
def correlation? = event.correlation_id.present?
|
|
40
|
+
|
|
41
|
+
# [attribute, before, after] per changed attribute; a bare value (not a
|
|
42
|
+
# [before, after] pair) is an "after" with no before.
|
|
43
|
+
def change_rows
|
|
44
|
+
return [] unless changes
|
|
45
|
+
|
|
46
|
+
changes.map do |attribute, pair|
|
|
47
|
+
before, after = pair.is_a?(Array) && pair.size == 2 ? pair : [nil, pair]
|
|
48
|
+
[attribute, before, after]
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def changes? = !changes.nil?
|
|
53
|
+
def changes_count = changes&.size.to_i
|
|
54
|
+
def snapshot? = !snapshot.nil?
|
|
55
|
+
def snapshot_rows = snapshot.to_a
|
|
56
|
+
|
|
57
|
+
# The event's snapshot is of something that went away.
|
|
58
|
+
def snapshot_of_removed? = event.event.end_with?(*SNAPSHOT_ENDINGS)
|
|
59
|
+
|
|
60
|
+
def other_rows = other.to_a
|
|
61
|
+
def other? = other.any?
|
|
62
|
+
def no_data? = changes.nil? && snapshot.nil? && other.empty?
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def data = @data ||= event.data.is_a?(Hash) ? event.data : {}
|
|
67
|
+
def changes = @changes ||= (data['changes'] if data['changes'].is_a?(Hash))
|
|
68
|
+
def snapshot = @snapshot ||= (data['snapshot'] if data['snapshot'].is_a?(Hash))
|
|
69
|
+
|
|
70
|
+
def other
|
|
71
|
+
@other ||= data.reject { |key, _| (key == 'changes' && changes) || (key == 'snapshot' && snapshot) }
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support/core_ext/numeric/time' # KNOWN_PEOPLE_WINDOW, read as this file loads
|
|
4
|
+
|
|
5
|
+
module PermissionForge
|
|
6
|
+
module Admin
|
|
7
|
+
# The admin's audit log: the events that match a filter, newest first,
|
|
8
|
+
# one page at a time, with no cap on how far back the pages go.
|
|
9
|
+
# Filtering is Audit::EventQuery's (the JSON API uses the same), so an
|
|
10
|
+
# invalid value raises Audit::EventQuery::Invalid naming the parameter.
|
|
11
|
+
#
|
|
12
|
+
# log = AuditLog.new(params) # every event
|
|
13
|
+
# log = AuditLog.new(params, scope: AuditLog.for_permission_set(set))
|
|
14
|
+
# log.page(params[:page]) # => AuditLog::Page
|
|
15
|
+
# log.relation # the whole result (exports)
|
|
16
|
+
class AuditLog
|
|
17
|
+
FILTERS = %w[from to event category actor_type actor_id subject_user_id permission_set correlation_id
|
|
18
|
+
record_type record_id record_uuid].freeze
|
|
19
|
+
PER_PAGE = 50
|
|
20
|
+
MAX_PAGE = 10_000_000
|
|
21
|
+
KNOWN_PEOPLE = 500
|
|
22
|
+
# known_people looks this far back (the occurred_at index serves it).
|
|
23
|
+
KNOWN_PEOPLE_WINDOW = 90.days
|
|
24
|
+
# Every view of the log writes one; left out of the default list and
|
|
25
|
+
# the timelines so they do not bury the changes (exports keep them).
|
|
26
|
+
VIEW_EVENT = 'audit.viewed'
|
|
27
|
+
|
|
28
|
+
Page = Struct.new(:events, :total, :number, :per_page, keyword_init: true) do
|
|
29
|
+
def pages = [(total + per_page - 1) / per_page, 1].max
|
|
30
|
+
def first_index = total.zero? ? 0 : ((number - 1) * per_page) + 1
|
|
31
|
+
def last_index = [number * per_page, total].min
|
|
32
|
+
def previous? = number > 1
|
|
33
|
+
def next? = number < pages
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Whether the audit table exists (the upgrade migration has run). The
|
|
37
|
+
# log can be read while recording is switched off; without the table
|
|
38
|
+
# there is nothing to read and every list is empty.
|
|
39
|
+
def self.table?
|
|
40
|
+
PermissionForge::AuditEvent.table_exists?
|
|
41
|
+
rescue StandardError
|
|
42
|
+
false
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Why the log is not recording, or nil when it records:
|
|
46
|
+
# :off audit_events is false (with or without the table)
|
|
47
|
+
# :failing audit_events is true, the table is missing and
|
|
48
|
+
# audit_change_failure_strategy is :raise — every
|
|
49
|
+
# permission change fails
|
|
50
|
+
# :missing_table the table is missing otherwise — nothing is recorded
|
|
51
|
+
def self.recording_problem(configuration = PermissionForge.configuration)
|
|
52
|
+
return :off if configuration.audit_events == false
|
|
53
|
+
return if table?
|
|
54
|
+
return :failing if configuration.audit_events == true && configuration.audit_change_failure_strategy == :raise
|
|
55
|
+
|
|
56
|
+
:missing_table
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The events of a Permission Set: its own, its resources', fields',
|
|
60
|
+
# actions', rules' and assignments' (permission_set_id is filled on all).
|
|
61
|
+
def self.for_permission_set(permission_set)
|
|
62
|
+
return PermissionForge::AuditEvent.none unless table?
|
|
63
|
+
|
|
64
|
+
PermissionForge::AuditEvent.where(permission_set_id: permission_set.id)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# The events about a person (their access) or by them (as the actor).
|
|
68
|
+
def self.for_person(user_id)
|
|
69
|
+
events = PermissionForge::AuditEvent
|
|
70
|
+
return events.none unless table?
|
|
71
|
+
|
|
72
|
+
events.where(subject_user_id: user_id.to_s).or(events.where(actor_type: 'user', actor_id: user_id.to_s))
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# The filter parameters that are set, by name. Values are kept as
|
|
76
|
+
# given (an array stays an array, so EventQuery can refuse it).
|
|
77
|
+
#
|
|
78
|
+
# @param params [ActionController::Parameters, Hash]
|
|
79
|
+
# @return [ActiveSupport::HashWithIndifferentAccess]
|
|
80
|
+
def self.filters(params)
|
|
81
|
+
indifferent(params).slice(*FILTERS).reject { |_, value| value.blank? }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def self.indifferent(params)
|
|
85
|
+
(params.respond_to?(:to_unsafe_h) ? params.to_unsafe_h : params.to_h).with_indifferent_access
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
attr_reader :filters
|
|
89
|
+
|
|
90
|
+
# @param params [ActionController::Parameters, Hash] see FILTERS; `views: '1'`
|
|
91
|
+
# keeps the views of the log in the list
|
|
92
|
+
# @param scope [ActiveRecord::Relation] the events to filter
|
|
93
|
+
# @param include_views [Boolean] keep the views of the log whatever the params say (exports)
|
|
94
|
+
def initialize(params = {}, scope: PermissionForge::AuditEvent.all, include_views: false)
|
|
95
|
+
@filters = self.class.filters(params)
|
|
96
|
+
@views_asked = self.class.indifferent(params)[:views].to_s == '1'
|
|
97
|
+
@include_views = include_views
|
|
98
|
+
@scope = scope
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Views of the log are listed when asked for, when the filter names an
|
|
102
|
+
# event or a category (whatever it names is shown), and in exports.
|
|
103
|
+
def include_views?
|
|
104
|
+
@include_views || @views_asked || @filters.key?('event') || @filters.key?('category')
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# The filters plus the views toggle, for links that keep the page's state.
|
|
108
|
+
def link_params
|
|
109
|
+
@views_asked ? @filters.merge('views' => '1') : @filters
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# @return [ActiveRecord::Relation] newest first
|
|
113
|
+
# @raise [Audit::EventQuery::Invalid]
|
|
114
|
+
def relation
|
|
115
|
+
@relation ||= PermissionForge::Audit::EventQuery.call(AuditLogDays.localize(@filters), scope: base_scope)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def base_scope
|
|
119
|
+
return @scope.none unless self.class.table?
|
|
120
|
+
|
|
121
|
+
include_views? ? @scope : @scope.where.not(event: VIEW_EVENT)
|
|
122
|
+
end
|
|
123
|
+
private :base_scope
|
|
124
|
+
|
|
125
|
+
# A page past the last one shows the last page.
|
|
126
|
+
#
|
|
127
|
+
# @param number [Object] the page parameter (1 when absent or not a number)
|
|
128
|
+
# @return [Page]
|
|
129
|
+
# @raise [Audit::EventQuery::Invalid]
|
|
130
|
+
def page(number, per_page: PER_PAGE)
|
|
131
|
+
total = relation.count
|
|
132
|
+
current = self.class.page_number(number, total, per_page)
|
|
133
|
+
events = relation.offset((current - 1) * per_page).limit(per_page).to_a
|
|
134
|
+
Page.new(events: events, total: total, number: current, per_page: per_page)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# 1 for anything that is not a number; at most the last page.
|
|
138
|
+
def self.page_number(number, total, per_page)
|
|
139
|
+
return 1 unless number.to_s.match?(/\A\d{1,9}\z/)
|
|
140
|
+
|
|
141
|
+
last = [(total + per_page - 1) / per_page, 1].max
|
|
142
|
+
number.to_i.clamp(1, [last, MAX_PAGE].min)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# People the log names lately (KNOWN_PEOPLE_WINDOW), for the actor and
|
|
146
|
+
# person filters' suggestions: [[id, label], ...]
|
|
147
|
+
# by id. Actors keep the name the event recorded; people who are only
|
|
148
|
+
# subjects are named from the user model when they still exist.
|
|
149
|
+
#
|
|
150
|
+
# @return [Array<Array(String, String)>]
|
|
151
|
+
def self.known_people(limit: KNOWN_PEOPLE, configuration: PermissionForge.configuration)
|
|
152
|
+
return [] unless table?
|
|
153
|
+
|
|
154
|
+
actors = actor_labels(limit)
|
|
155
|
+
people = actors.merge(subject_names(subject_ids(limit) - actors.keys, configuration))
|
|
156
|
+
people.sort_by { |id, _| [id.to_s.rjust(20, '0'), id.to_s] }.first(limit)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# { actor id => the name the events recorded } of people acting lately.
|
|
160
|
+
def self.actor_labels(limit)
|
|
161
|
+
recent.where(actor_type: 'user').where.not(actor_id: nil).group(:actor_id).limit(limit).maximum(:actor_label)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def self.subject_ids(limit)
|
|
165
|
+
recent.where.not(subject_user_id: nil).group(:subject_user_id).limit(limit).count.keys
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def self.recent
|
|
169
|
+
PermissionForge::AuditEvent.where(occurred_at: KNOWN_PEOPLE_WINDOW.ago..)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# { id => label (nil when the person is gone) } for every id.
|
|
173
|
+
def self.subject_names(ids, configuration)
|
|
174
|
+
return {} if ids.empty?
|
|
175
|
+
|
|
176
|
+
found = configuration.user_model.where(id: ids).to_h { |user| [user.id.to_s, PersonLabel.new(user).name] }
|
|
177
|
+
ids.index_with { |id| found[id.to_s] }
|
|
178
|
+
rescue StandardError
|
|
179
|
+
ids.index_with(nil)
|
|
180
|
+
end
|
|
181
|
+
private_class_method :actor_labels, :subject_ids, :subject_names, :recent
|
|
182
|
+
|
|
183
|
+
# Every catalogued event name, by category (the event filter).
|
|
184
|
+
# @return [Hash{String => Array<String>}]
|
|
185
|
+
def self.event_names_by_category
|
|
186
|
+
PermissionForge::Audit::EventCatalog.entries.values.group_by(&:category)
|
|
187
|
+
.transform_values { |entries| entries.map(&:name).sort }
|
|
188
|
+
.sort_by { |category, _| PermissionForge::AuditEvent::CATEGORIES.index(category) || 99 }
|
|
189
|
+
.to_h
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
# The audit log page shows dates in the admin's time zone (Time.zone), so
|
|
6
|
+
# a date-only "from" / "to" means that day there: from its first to its
|
|
7
|
+
# last microsecond, with the offset written out for Audit::EventQuery
|
|
8
|
+
# (which reads a bare date as a UTC day — the JSON API keeps that).
|
|
9
|
+
# Anything else (a timestamp, an impossible date, a list) is left for
|
|
10
|
+
# EventQuery to accept or refuse.
|
|
11
|
+
module AuditLogDays
|
|
12
|
+
DATE = /\A(\d{4})-(\d{2})-(\d{2})\z/
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
# @param filters [Hash] the filters as given (string keys)
|
|
17
|
+
# @param zone [ActiveSupport::TimeZone]
|
|
18
|
+
# @return [Hash] the same, date-only from / to as local-day timestamps
|
|
19
|
+
def localize(filters, zone: Time.zone)
|
|
20
|
+
filters.merge(%w[from to].each_with_object({}) do |key, out|
|
|
21
|
+
day = local_day(filters[key], zone)
|
|
22
|
+
out[key] = (key == 'from' ? day.beginning_of_day : day.end_of_day).iso8601(6) if day
|
|
23
|
+
end)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def local_day(value, zone)
|
|
27
|
+
match = value.is_a?(String) && DATE.match(value)
|
|
28
|
+
return nil unless match && zone
|
|
29
|
+
|
|
30
|
+
year, month, day = match.captures.map(&:to_i)
|
|
31
|
+
zone.local(year, month, day) if year.positive? && Date.valid_date?(year, month, day)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# The zone's name for the page and the export metadata ("Pacific/Auckland").
|
|
35
|
+
def zone_name(zone = Time.zone)
|
|
36
|
+
name = zone.tzinfo&.name || zone.name
|
|
37
|
+
name == 'Etc/UTC' ? 'UTC' : name
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
# The people, Permission Sets and Permission Set Groups a page of audit
|
|
6
|
+
# events names, loaded once for the page (one query per kind) so every
|
|
7
|
+
# row can say "Ana gave Bo the Permission Set Sales". A record that is
|
|
8
|
+
# gone is simply not found: the event's own label snapshots stand in.
|
|
9
|
+
#
|
|
10
|
+
# A set or group is named as it was when the event happened
|
|
11
|
+
# (#permission_set_name, #group_name): the label of its own latest event
|
|
12
|
+
# at or before that moment (a create, update or destroy keeps the name it
|
|
13
|
+
# had then), else the name its earliest later rename started from, else
|
|
14
|
+
# its name today (AuditNameHistory; no new column, a bounded read).
|
|
15
|
+
class AuditLookups
|
|
16
|
+
GROUP_KEYS = %w[permission_set_group_id].freeze
|
|
17
|
+
|
|
18
|
+
# @param events [Array<AuditEvent>]
|
|
19
|
+
def initialize(events, configuration: PermissionForge.configuration)
|
|
20
|
+
@events = Array(events)
|
|
21
|
+
@configuration = configuration
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @param id [String, Integer, nil] a user id
|
|
25
|
+
# @return [Object, nil] the host user
|
|
26
|
+
def person(id)
|
|
27
|
+
people[id.to_s] if id.present?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @return [PermissionForge::PermissionSet, nil]
|
|
31
|
+
def permission_set(id)
|
|
32
|
+
permission_sets[id.to_i] if id.present?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The Permission Set Group of a membership event (from its data).
|
|
36
|
+
# @return [PermissionForge::PermissionSetGroup, nil]
|
|
37
|
+
def group(event)
|
|
38
|
+
id = self.class.group_id(event)
|
|
39
|
+
groups[id.to_i] if id
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# The Permission Set's name when `event` happened (nil: unknown).
|
|
43
|
+
# @return [String, nil]
|
|
44
|
+
def permission_set_name(event)
|
|
45
|
+
id = event.permission_set_id
|
|
46
|
+
return if id.blank?
|
|
47
|
+
|
|
48
|
+
set_history.name_as_of(id, event) || permission_set(id)&.name
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The name of a membership event's group when the event happened.
|
|
52
|
+
# @return [String, nil]
|
|
53
|
+
def group_name(event)
|
|
54
|
+
id = self.class.group_id(event)
|
|
55
|
+
return unless id
|
|
56
|
+
|
|
57
|
+
group_history.name_as_of(id, event) || groups[id.to_i]&.name
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.group_id(event)
|
|
61
|
+
data = event.data || {}
|
|
62
|
+
snapshot = data['snapshot'].is_a?(Hash) ? data['snapshot'] : {}
|
|
63
|
+
changes = data['changes'].is_a?(Hash) ? data['changes'] : {}
|
|
64
|
+
GROUP_KEYS.each do |key|
|
|
65
|
+
value = snapshot[key] || Array(changes[key]).last
|
|
66
|
+
return value if value.present?
|
|
67
|
+
end
|
|
68
|
+
nil
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def people
|
|
74
|
+
@people ||= begin
|
|
75
|
+
ids = @events.flat_map { |e| [e.subject_user_id, (e.actor_id if e.actor_type == 'user')] }.compact_blank.uniq
|
|
76
|
+
ids.empty? ? {} : @configuration.user_model.where(id: ids).index_by { |user| user.id.to_s }
|
|
77
|
+
rescue StandardError
|
|
78
|
+
{}
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def permission_sets
|
|
83
|
+
@permission_sets ||= find_by_ids(PermissionForge::PermissionSet, @events.map(&:permission_set_id))
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def groups
|
|
87
|
+
@groups ||= find_by_ids(PermissionForge::PermissionSetGroup, @events.map { |e| self.class.group_id(e) })
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def find_by_ids(model, ids)
|
|
91
|
+
ids = ids.compact.map(&:to_i).uniq
|
|
92
|
+
ids.empty? ? {} : model.where(id: ids).index_by(&:id)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def set_history
|
|
96
|
+
@set_history ||= AuditNameHistory.new(PermissionForge::PermissionSet, @events.map(&:permission_set_id), @events)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def group_history
|
|
100
|
+
@group_history ||= AuditNameHistory.new(PermissionForge::PermissionSetGroup,
|
|
101
|
+
@events.map { |e| self.class.group_id(e) }, @events)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
# The name a Permission Set or group had when each event of a page
|
|
6
|
+
# happened, from the label snapshots of its own events (a create, update
|
|
7
|
+
# or destroy keeps the name it had then): the latest at or before the
|
|
8
|
+
# event, else what its first recorded rename started from (or that
|
|
9
|
+
# event's own label), else nil (the caller falls back to today's name).
|
|
10
|
+
#
|
|
11
|
+
# Reads a bounded slice, never the whole history: the record's events
|
|
12
|
+
# inside the page's time span and its one latest event before it (two
|
|
13
|
+
# plucks of four columns), plus one full event per record that has no
|
|
14
|
+
# earlier event at all (for its `changes`).
|
|
15
|
+
class AuditNameHistory
|
|
16
|
+
Entry = Struct.new(:record_id, :occurred_at, :id, :record_label)
|
|
17
|
+
COLUMNS = %i[record_id occurred_at id record_label].freeze
|
|
18
|
+
|
|
19
|
+
# @param model [Class] PermissionSet or PermissionSetGroup
|
|
20
|
+
# @param ids [Array] the record ids the page names
|
|
21
|
+
# @param events [Array<AuditEvent>] the page
|
|
22
|
+
def initialize(model, ids, events)
|
|
23
|
+
@ids = ids.compact_blank.map(&:to_s).uniq
|
|
24
|
+
@first, @last = events.filter_map(&:occurred_at).minmax
|
|
25
|
+
@scope = PermissionForge::AuditEvent.where(record_type: model.base_class.name, record_id: @ids)
|
|
26
|
+
@first_events = {}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @return [String, nil]
|
|
30
|
+
def name_as_of(id, event)
|
|
31
|
+
return if @ids.empty? || @first.nil? || event.occurred_at.nil?
|
|
32
|
+
|
|
33
|
+
rows = entries.fetch(id.to_s, [])
|
|
34
|
+
earlier = latest_at_or_before(rows, [event.occurred_at, event.id || 0])
|
|
35
|
+
return earlier.record_label.presence if earlier
|
|
36
|
+
|
|
37
|
+
name_before(first_event(id.to_s, rows))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def latest_at_or_before(rows, moment)
|
|
43
|
+
rows.reverse_each.find { |row| ([row.occurred_at, row.id] <=> moment) <= 0 }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# { "id" => [Entry, ...] oldest first }
|
|
47
|
+
def entries
|
|
48
|
+
@entries ||= (span + latest_before_span).uniq(&:id).sort_by { |row| [row.occurred_at, row.id] }.group_by(&:record_id)
|
|
49
|
+
rescue ActiveRecord::ActiveRecordError
|
|
50
|
+
{}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def span = pluck(@scope.where(occurred_at: @first..@last))
|
|
54
|
+
|
|
55
|
+
def latest_before_span
|
|
56
|
+
latest = @scope.where(occurred_at: ...@first).group(:record_id).maximum(:occurred_at)
|
|
57
|
+
return [] if latest.empty?
|
|
58
|
+
|
|
59
|
+
pluck(latest.map { |id, at| @scope.where(record_id: id, occurred_at: at) }.reduce(:or))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def pluck(relation) = relation.pluck(*COLUMNS).map { |values| Entry.new(*values) }
|
|
63
|
+
|
|
64
|
+
# The record's first recorded event (it has none at or before the
|
|
65
|
+
# event asking): the first one in the span, else the first after it.
|
|
66
|
+
def first_event(id, rows)
|
|
67
|
+
return @first_events[id] if @first_events.key?(id)
|
|
68
|
+
|
|
69
|
+
@first_events[id] =
|
|
70
|
+
if rows.any? then PermissionForge::AuditEvent.find_by(id: rows.first.id)
|
|
71
|
+
else @scope.where(record_id: id).where(occurred_at: @last..).order(:occurred_at, :id).first
|
|
72
|
+
end
|
|
73
|
+
rescue ActiveRecord::ActiveRecordError
|
|
74
|
+
@first_events[id] = nil
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def name_before(event)
|
|
78
|
+
return unless event
|
|
79
|
+
|
|
80
|
+
renamed = event.data.dig('changes', 'name')
|
|
81
|
+
renamed.is_a?(Array) ? renamed.first.presence : event.record_label.presence
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
# Who created and who last edited each permission record. Creation comes
|
|
6
|
+
# from created_by / created_at (HasAuditTrail); the last edit from the
|
|
7
|
+
# record's latest change event when the audit log has one (LastChanges),
|
|
8
|
+
# else from updated_by / updated_at — an event names the real editor,
|
|
9
|
+
# while updated_by keeps the previous one across an edit by nobody, an
|
|
10
|
+
# HTTP Basic login or an API client. The full history is the audit log
|
|
11
|
+
# (Admin::AuditLog).
|
|
12
|
+
class AuditTrail
|
|
13
|
+
TYPES = {
|
|
14
|
+
'permission_set' => 'PermissionForge::PermissionSet',
|
|
15
|
+
'resource' => 'PermissionForge::Resource',
|
|
16
|
+
'field' => 'PermissionForge::ResourceField',
|
|
17
|
+
'action' => 'PermissionForge::ResourceAction',
|
|
18
|
+
'rule' => 'PermissionForge::PermissionSetRule',
|
|
19
|
+
'assignment' => 'PermissionForge::PermissionSetUser',
|
|
20
|
+
'group' => 'PermissionForge::PermissionSetGroup',
|
|
21
|
+
'group_membership' => 'PermissionForge::PermissionSetGroupUser'
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
DAY_FILTERS = [7, 30, 90].freeze
|
|
25
|
+
CSV_HEADER = ['Type', 'Record', 'Where', 'Created by', 'Created at', 'Last edited by', 'Last edited at'].freeze
|
|
26
|
+
|
|
27
|
+
# `last_event` — the record's latest change event, or nil (no events:
|
|
28
|
+
# auditing off, or the record predates the audit log).
|
|
29
|
+
Entry = Struct.new(:type, :record, :last_event, keyword_init: true) do
|
|
30
|
+
def created_by = record.try(:created_by)
|
|
31
|
+
def updated_by = record.try(:updated_by)
|
|
32
|
+
def created_at = record.created_at
|
|
33
|
+
def created_by_id = record.try(:created_by_id)
|
|
34
|
+
def updated_by_id = record.try(:updated_by_id)
|
|
35
|
+
|
|
36
|
+
# When the last edit happened: the event's time when there is one.
|
|
37
|
+
def updated_at = last_event ? last_event.occurred_at : record.updated_at
|
|
38
|
+
|
|
39
|
+
# The last editor's user id as a String, or nil (not a person, or unknown).
|
|
40
|
+
def editor_id
|
|
41
|
+
return updated_by_id&.to_s unless last_event
|
|
42
|
+
|
|
43
|
+
last_event.actor_id if last_event.actor_type == 'user'
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.type_of(record)
|
|
48
|
+
TYPES.key(record.class.name)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# People who appear as creator/editor in the given entries.
|
|
52
|
+
def self.editors(entries)
|
|
53
|
+
entries.flat_map { |e| [e.created_by, e.updated_by] }.compact.uniq(&:id)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Records a person created or last edited, grouped by type, most
|
|
57
|
+
# recently edited first. `limit` caps each type (the admin page);
|
|
58
|
+
# nil lists everything (the CSV export).
|
|
59
|
+
# @return [Hash{String => Array<Entry>}]
|
|
60
|
+
def changes_by(user_id, limit: nil)
|
|
61
|
+
TYPES.each_with_object({}) do |(type, class_name), out|
|
|
62
|
+
records = touched_records(class_name.constantize, user_id, limit)
|
|
63
|
+
entries = entries_for(records.map { |record| [type, record] }).select { |entry| made_by?(entry, user_id) }
|
|
64
|
+
out[type] = entries if entries.any?
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Whether the last changes_by(limit:) read as many rows as the limit
|
|
69
|
+
# for some type (more may exist), whatever it kept of them.
|
|
70
|
+
def capped?
|
|
71
|
+
@capped == true
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Every record belonging to a Permission Set (the set, its resources,
|
|
75
|
+
# fields, actions, rules and assignments), filtered and sorted.
|
|
76
|
+
# Filters (all optional): who:, type:, days:, sort: ('asc' / 'desc', default 'desc').
|
|
77
|
+
# @return [Array<Entry>]
|
|
78
|
+
def for_permission_set(permission_set, **filters)
|
|
79
|
+
entries = apply_filters(permission_set_entries(permission_set), filters)
|
|
80
|
+
sorted = entries.sort_by { |e| e.updated_at || e.created_at || Time.at(0) }
|
|
81
|
+
filters[:sort].to_s == 'asc' ? sorted : sorted.reverse
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# @param describe [#call] (entry) -> [type_label, record_label, where]
|
|
85
|
+
# @param person [#call] (entry) -> [created by, last edited by]
|
|
86
|
+
def to_csv(entries, describe:, person:)
|
|
87
|
+
Csv.document([CSV_HEADER] + entries.map { |entry| csv_row(entry, describe, person) })
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def csv_row(entry, describe, person)
|
|
93
|
+
type_label, label, where = describe.call(entry)
|
|
94
|
+
created_by, edited_by = person.call(entry)
|
|
95
|
+
[type_label, label, where, created_by, entry.created_at&.iso8601, edited_by, entry.updated_at&.iso8601]
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# The records a person created or was stamped as editing, latest edit
|
|
99
|
+
# first; notes when the limit was reached (capped?).
|
|
100
|
+
def touched_records(model, user_id, limit)
|
|
101
|
+
records = with_people(touched_by(model, user_id).order(updated_at: :desc).limit(limit)).to_a
|
|
102
|
+
@capped = true if limit && records.size >= limit
|
|
103
|
+
records
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def touched_by(model, user_id)
|
|
107
|
+
model.where(created_by_id: user_id).or(model.where(updated_by_id: user_id))
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Created it, or made its last edit (by the event when there is one).
|
|
111
|
+
def made_by?(entry, user_id)
|
|
112
|
+
entry.created_by_id.to_s == user_id.to_s || entry.editor_id == user_id.to_s
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def apply_filters(entries, filters)
|
|
116
|
+
type, who, days = filters.values_at(:type, :who, :days)
|
|
117
|
+
entries = entries.select { |e| e.type == type } if type.present? && TYPES.key?(type)
|
|
118
|
+
entries = filter_who(entries, who) if who.present?
|
|
119
|
+
entries = filter_days(entries, days.to_i) if DAY_FILTERS.include?(days.to_i)
|
|
120
|
+
entries
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def with_people(scope)
|
|
124
|
+
scope.includes(:created_by, :updated_by)
|
|
125
|
+
rescue StandardError
|
|
126
|
+
scope
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def permission_set_entries(permission_set)
|
|
130
|
+
resources = with_people(permission_set.resources).to_a
|
|
131
|
+
records = [['permission_set', permission_set]] + resources.map { |r| ['resource', r] } +
|
|
132
|
+
resource_child_records(resources.map(&:id)) + permission_set_child_records(permission_set)
|
|
133
|
+
entries_for(records)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def permission_set_child_records(permission_set)
|
|
137
|
+
typed('rule', permission_set.permission_set_rules.includes(:resource)) +
|
|
138
|
+
typed('assignment', permission_set.permission_set_users.includes(:user))
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def resource_child_records(resource_ids)
|
|
142
|
+
typed('field', PermissionForge::ResourceField.where(resource_id: resource_ids).includes(:resource)) +
|
|
143
|
+
typed('action', PermissionForge::ResourceAction.where(resource_id: resource_ids).includes(:resource))
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def typed(type, scope)
|
|
147
|
+
with_people(scope).map { |record| [type, record] }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# [[type, record], ...] → entries, each with its latest change event.
|
|
151
|
+
def entries_for(typed_records)
|
|
152
|
+
last = LastChanges.new(typed_records.map(&:last))
|
|
153
|
+
typed_records.map { |type, record| Entry.new(type: type, record: record, last_event: last.last_edit(record)) }
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def filter_who(entries, who)
|
|
157
|
+
entries.select { |e| e.created_by_id.to_s == who.to_s || e.editor_id == who.to_s }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def filter_days(entries, days)
|
|
161
|
+
cutoff = days.days.ago
|
|
162
|
+
entries.select do |e|
|
|
163
|
+
time = e.updated_at || e.created_at
|
|
164
|
+
time && time >= cutoff
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|