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,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Api
|
|
5
|
+
module Serializers
|
|
6
|
+
class PermissionSetUserSerializer < BaseSerializer
|
|
7
|
+
attributes :id, :uuid, :permission_set_id, :user_id, :expires_at, :created_at, :updated_at
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Api
|
|
5
|
+
module Serializers
|
|
6
|
+
class ResourceActionSerializer < BaseSerializer
|
|
7
|
+
attributes :id, :uuid, :resource_id, :name, :label, :description,
|
|
8
|
+
:is_permitted, :is_active,
|
|
9
|
+
:created_at, :updated_at
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Api
|
|
5
|
+
module Serializers
|
|
6
|
+
class ResourceFieldSerializer < BaseSerializer
|
|
7
|
+
attributes :id, :uuid, :resource_id, :name, :label, :description,
|
|
8
|
+
:is_active, :is_editable,
|
|
9
|
+
:read_access, :edit_access, :mask_access, :mask_strategy,
|
|
10
|
+
:created_at, :updated_at
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Api
|
|
5
|
+
module Serializers
|
|
6
|
+
class ResourceSerializer < BaseSerializer
|
|
7
|
+
attributes :id, :uuid, :permission_set_id, :name, :description,
|
|
8
|
+
:is_active, :is_editable,
|
|
9
|
+
:read_access, :create_access, :edit_access, :delete_access,
|
|
10
|
+
:view_all, :modify_all,
|
|
11
|
+
:created_at, :updated_at
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Audit
|
|
5
|
+
# Who did it, as stored on an event: the type (user, api, basic_auth,
|
|
6
|
+
# system, unknown), the id as a string (bigint and UUID keys both fit) and
|
|
7
|
+
# a label — the person's name as it was at the time.
|
|
8
|
+
#
|
|
9
|
+
# Resolution order (Actor.current): the actor in the audit context (an
|
|
10
|
+
# explicit `PermissionForge.with_audit_actor` block, or the request
|
|
11
|
+
# context the controllers set), then `PermissionForge.current_user`,
|
|
12
|
+
# otherwise unknown.
|
|
13
|
+
#
|
|
14
|
+
# @api public
|
|
15
|
+
class Actor
|
|
16
|
+
TYPES = %w[user api basic_auth system unknown].freeze
|
|
17
|
+
|
|
18
|
+
# The actor's type (`user`, `system`, `unknown`, ...), id and display label, as stored on the event.
|
|
19
|
+
attr_reader :type, :id, :label
|
|
20
|
+
|
|
21
|
+
# The class of the user object the actor was made from (for_user), nil
|
|
22
|
+
# for an actor given as a Hash or built by hand. Not part of equality:
|
|
23
|
+
# it says whether the id is a key of config.audit_user_class
|
|
24
|
+
# (HasAuditTrail stamps created_by / updated_by only then).
|
|
25
|
+
attr_reader :user_class
|
|
26
|
+
|
|
27
|
+
# @raise [ArgumentError] for a type outside TYPES
|
|
28
|
+
def initialize(type:, id: nil, label: nil, user_class: nil)
|
|
29
|
+
type = type.to_s
|
|
30
|
+
raise ArgumentError, "audit actor type must be one of #{TYPES.join(', ')} (got #{type.inspect})" unless TYPES.include?(type)
|
|
31
|
+
|
|
32
|
+
@type = type
|
|
33
|
+
@id = id&.to_s
|
|
34
|
+
@label = label.presence&.to_s
|
|
35
|
+
@user_class = user_class
|
|
36
|
+
freeze
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class << self
|
|
40
|
+
# @return [Actor] never nil
|
|
41
|
+
def current
|
|
42
|
+
Context[:actor] || from_current_user || unknown
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @param user [Object] a host user (anything with an id)
|
|
46
|
+
# @param label [String, nil] overrides the name snapshot
|
|
47
|
+
# @param type [String] 'user' unless the user acts as an API principal
|
|
48
|
+
def for_user(user, label: nil, type: 'user')
|
|
49
|
+
new(type: type, id: user.id, label: label || label_for(user), user_class: record_class_of(user))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The class of the record behind a decorated user (SimpleDelegator /
|
|
53
|
+
# Delegator, Draper), so a host's decorated current_user still stamps.
|
|
54
|
+
def record_class_of(user)
|
|
55
|
+
inner = user
|
|
56
|
+
3.times do
|
|
57
|
+
if inner.respond_to?(:__getobj__) then inner = inner.__getobj__
|
|
58
|
+
elsif inner.respond_to?(:decorated?) && inner.respond_to?(:object) && inner.decorated? then inner = inner.object
|
|
59
|
+
else break
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
inner.class
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @return [Actor] the actor used when nothing identifies who acted
|
|
66
|
+
def unknown
|
|
67
|
+
new(type: 'unknown')
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# @param label [String] what ran (a job, a rake task)
|
|
71
|
+
# @return [Actor] a `system` actor
|
|
72
|
+
def system(label)
|
|
73
|
+
new(type: 'system', label: label)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# An Actor, a Hash { type:, id:, label: }, a user, or nil.
|
|
77
|
+
#
|
|
78
|
+
# @return [Actor, nil]
|
|
79
|
+
def coerce(value)
|
|
80
|
+
case value
|
|
81
|
+
when nil then nil
|
|
82
|
+
when Actor then value
|
|
83
|
+
when Hash then new(**value.symbolize_keys.slice(:type, :id, :label))
|
|
84
|
+
else for_user(value)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# The name snapshot for a user: config.user_label_formatter, else the
|
|
89
|
+
# same readers the admin screens use (Admin::PersonLabel).
|
|
90
|
+
def label_for(user)
|
|
91
|
+
PermissionForge::Admin::PersonLabel.new(user).name
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def from_current_user
|
|
97
|
+
user = resolve_current_user
|
|
98
|
+
for_user(user) if user
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# A current_user_resolver that raises (a Current attribute reset, a
|
|
102
|
+
# session store that is down) must not turn every change into an
|
|
103
|
+
# error: the actor is unknown and the failure is reported
|
|
104
|
+
# (config.audit_hook_failure_strategy; :raise raises). A user it
|
|
105
|
+
# returns that cannot be read is a failure of the event itself.
|
|
106
|
+
def resolve_current_user
|
|
107
|
+
PermissionForge.current_user
|
|
108
|
+
rescue StandardError => e
|
|
109
|
+
Failure.handle(e, strategy: PermissionForge.configuration.audit_hook_failure_strategy, operation: :actor)
|
|
110
|
+
nil
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Whether the actor is a record of `klass` (or a subclass): made from a
|
|
115
|
+
# user object of that class, not merely carrying an id.
|
|
116
|
+
#
|
|
117
|
+
# @param klass [Class, nil]
|
|
118
|
+
def user_of?(klass)
|
|
119
|
+
!klass.nil? && type == 'user' && !id.nil? && user_class.is_a?(Class) && user_class <= klass
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# The three actor columns of an event.
|
|
123
|
+
def to_columns
|
|
124
|
+
{ actor_type: type, actor_id: id, actor_label: label }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# [type, id, label]
|
|
128
|
+
def to_a
|
|
129
|
+
[type, id, label]
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Two actors are equal when their type, id and label are.
|
|
133
|
+
def ==(other)
|
|
134
|
+
other.is_a?(Actor) && to_a == other.to_a
|
|
135
|
+
end
|
|
136
|
+
alias eql? ==
|
|
137
|
+
|
|
138
|
+
# @return [Integer] consistent with {#==}
|
|
139
|
+
def hash
|
|
140
|
+
to_a.hash
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Audit
|
|
5
|
+
# Who acts in an admin UI or API request — never taken from what the
|
|
6
|
+
# client sends (parameters, headers), only from what the host and the
|
|
7
|
+
# engine have verified:
|
|
8
|
+
#
|
|
9
|
+
# 1. config.audit_actor_resolver, called with the controller (a user, an
|
|
10
|
+
# Actor or a Hash { type:, id:, label: }; nil: it does not know)
|
|
11
|
+
# 2. the signed-in host user (config.current_user_resolver)
|
|
12
|
+
# 3. admin UI under HTTP Basic: the username that was checked, as
|
|
13
|
+
# actor_type 'basic_auth' with no id
|
|
14
|
+
#
|
|
15
|
+
# Nobody named: the request's fallback — unknown in the admin UI, an
|
|
16
|
+
# anonymous API client in the API (api_auth said yes, but not who).
|
|
17
|
+
#
|
|
18
|
+
# A step that raises, or answers with something that is no actor, makes
|
|
19
|
+
# the actor unknown and is reported (Audit::Failure, under
|
|
20
|
+
# config.audit_hook_failure_strategy): the request goes on.
|
|
21
|
+
#
|
|
22
|
+
# @api private
|
|
23
|
+
class ActorResolver
|
|
24
|
+
# The request env key under which BasicAuthentication leaves the
|
|
25
|
+
# username it has just verified. Not reachable from a header (those are
|
|
26
|
+
# HTTP_*).
|
|
27
|
+
BASIC_AUTH_ENV = 'permission_forge.audit.basic_auth_user'
|
|
28
|
+
API_CLIENT_LABEL = 'API client'
|
|
29
|
+
|
|
30
|
+
# The steps tried, in order, per request source; the first that names
|
|
31
|
+
# someone wins.
|
|
32
|
+
STEPS = {
|
|
33
|
+
'admin_ui' => %i[configured_actor current_user basic_auth_user],
|
|
34
|
+
'api' => %i[configured_actor current_user]
|
|
35
|
+
}.freeze
|
|
36
|
+
|
|
37
|
+
# Who acts when no step names anyone.
|
|
38
|
+
FALLBACKS = {
|
|
39
|
+
'admin_ui' => -> { Actor.unknown },
|
|
40
|
+
'api' => -> { Actor.new(type: 'api', label: API_CLIENT_LABEL) }
|
|
41
|
+
}.freeze
|
|
42
|
+
|
|
43
|
+
# @param controller [ActionController::Metal]
|
|
44
|
+
# @param source [String] 'admin_ui' or 'api'
|
|
45
|
+
# @param fallback [Boolean] false: unknown instead of the source's
|
|
46
|
+
# fallback when nobody is named (a refused request)
|
|
47
|
+
# @return [Actor] never nil
|
|
48
|
+
def self.call(controller, source:, fallback: true, configuration: PermissionForge.configuration)
|
|
49
|
+
new(controller, configuration).call(source, fallback: fallback)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def initialize(controller, configuration)
|
|
53
|
+
@controller = controller
|
|
54
|
+
@configuration = configuration
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @return [Actor]
|
|
58
|
+
def call(source, fallback: true)
|
|
59
|
+
identify(source) || (fallback ? FALLBACKS.fetch(source).call : Actor.unknown)
|
|
60
|
+
rescue StandardError => e
|
|
61
|
+
Failure.handle(e, strategy: @configuration.audit_hook_failure_strategy, operation: :actor, source: source)
|
|
62
|
+
Actor.unknown
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Who the request names, without a fallback. Raises what a step raises.
|
|
66
|
+
#
|
|
67
|
+
# @return [Actor, nil]
|
|
68
|
+
def identify(source)
|
|
69
|
+
STEPS.fetch(source).each do |step|
|
|
70
|
+
actor = send(step)
|
|
71
|
+
return actor if actor
|
|
72
|
+
end
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def configured_actor
|
|
79
|
+
resolver = @configuration.audit_actor_resolver
|
|
80
|
+
coerce(resolver.call(@controller)) if resolver
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def current_user
|
|
84
|
+
coerce(PermissionForge.current_user)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Set by BasicAuthentication only after the credentials matched.
|
|
88
|
+
def basic_auth_user
|
|
89
|
+
username = @controller.request.env[BASIC_AUTH_ENV]
|
|
90
|
+
Actor.new(type: 'basic_auth', label: username) if username.present?
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# A user without an id names nobody; anything Actor.coerce cannot read
|
|
94
|
+
# raises (and so counts as a failure).
|
|
95
|
+
def coerce(value)
|
|
96
|
+
actor = Actor.coerce(value)
|
|
97
|
+
return if actor.nil? || (actor.type == 'user' && actor.id.nil?)
|
|
98
|
+
|
|
99
|
+
actor
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Audit
|
|
5
|
+
# The read-only admin pages that leave a trace: who looked at someone's
|
|
6
|
+
# effective access, who read or exported the audit trail. One registry,
|
|
7
|
+
# keyed by "controller_path#action"; the base controllers record the
|
|
8
|
+
# first entry that applies after a successful (2xx) response — the
|
|
9
|
+
# controllers themselves are not touched.
|
|
10
|
+
#
|
|
11
|
+
# Adding a page is one call:
|
|
12
|
+
#
|
|
13
|
+
# AuditedViews.register('permission_forge/audit_events#index', 'audit.viewed',
|
|
14
|
+
# only_if: ->(c) { c.request.format.html? },
|
|
15
|
+
# payload: ->(c) { { data: { 'filters' => c.params.to_unsafe_h.slice('from', 'to') } } })
|
|
16
|
+
#
|
|
17
|
+
# `only_if` (optional) decides whether the entry applies to the request;
|
|
18
|
+
# `payload` (optional) returns the keys passed to Audit.record (record:,
|
|
19
|
+
# subject_user_id:, permission_set:, data: …). Actor, source, request id
|
|
20
|
+
# and IP come from the request's audit context.
|
|
21
|
+
#
|
|
22
|
+
# @api private
|
|
23
|
+
module AuditedViews
|
|
24
|
+
View = Struct.new(:event, :only_if, :payload, keyword_init: true) do
|
|
25
|
+
def applies?(controller)
|
|
26
|
+
only_if.nil? || only_if.call(controller) ? true : false
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def payload_for(controller)
|
|
30
|
+
payload ? payload.call(controller) : {}
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
@entries = {}
|
|
35
|
+
|
|
36
|
+
class << self
|
|
37
|
+
# @param key [String] "controller_path#action", e.g. 'permission_forge/people#show'
|
|
38
|
+
# @param event [String] an admin, change or system event of the catalogue
|
|
39
|
+
# @param only_if [#call, nil] lambda(controller) → whether the entry applies
|
|
40
|
+
# @param payload [#call, nil] lambda(controller) → Hash for Audit.record
|
|
41
|
+
# @raise [EventCatalog::UnknownEventError, ArgumentError]
|
|
42
|
+
def register(key, event, only_if: nil, payload: nil)
|
|
43
|
+
entry = EventCatalog.fetch(event)
|
|
44
|
+
raise ArgumentError, "#{event} is an access event: views record admin events" if entry.runtime?
|
|
45
|
+
|
|
46
|
+
(@entries[key.to_s] ||= []) << View.new(event: entry.name, only_if: only_if, payload: payload)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Removes every entry of a key, or only those of one event.
|
|
50
|
+
def unregister(key, event = nil)
|
|
51
|
+
views = @entries[key.to_s]
|
|
52
|
+
return unless views
|
|
53
|
+
|
|
54
|
+
event ? views.reject! { |view| view.event == event } : views.clear
|
|
55
|
+
@entries.delete(key.to_s) if views.empty?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @return [Hash{String => Array<View>}]
|
|
59
|
+
def entries
|
|
60
|
+
@entries.transform_values(&:dup)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @return [Array<View>]
|
|
64
|
+
def entries_for(key)
|
|
65
|
+
Array(@entries[key.to_s]).dup
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @return [View, nil] the first entry that applies to the request
|
|
69
|
+
def match(controller)
|
|
70
|
+
views = @entries["#{controller.controller_path}##{controller.action_name}"]
|
|
71
|
+
views&.find { |view| view.applies?(controller) }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Records the view when the response succeeded and an entry applies.
|
|
75
|
+
# A HEAD request shows nothing and is not recorded.
|
|
76
|
+
#
|
|
77
|
+
# @return [AuditEvent, nil]
|
|
78
|
+
def record(controller)
|
|
79
|
+
return if controller.request.head? || !controller.response.successful?
|
|
80
|
+
|
|
81
|
+
view = match(controller)
|
|
82
|
+
Audit.record(view.event, **view.payload_for(controller)) if view
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Shared pieces of the built-in entries. What was shown is decided by
|
|
87
|
+
# the response (its media type), not by what the client asked for: an
|
|
88
|
+
# Accept header can negotiate a CSV without a .csv in the path.
|
|
89
|
+
module Requests
|
|
90
|
+
AUDIT_FILTERS = %w[who type days sort].freeze
|
|
91
|
+
LOG_FILTERS = %w[from to event category actor_type actor_id subject_user_id permission_set correlation_id
|
|
92
|
+
record_type record_id record_uuid].freeze
|
|
93
|
+
API_FILTERS = %w[from to event category actor_type actor_id subject_user_id permission_set record_type record_id
|
|
94
|
+
record_uuid correlation_id page per_page].freeze
|
|
95
|
+
VALUE_LIMIT = 255
|
|
96
|
+
|
|
97
|
+
module_function
|
|
98
|
+
|
|
99
|
+
def csv?(controller)
|
|
100
|
+
controller.response.media_type == Mime[:csv].to_s
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# 'csv' / 'json' for a file the response handed over, nil for a page.
|
|
104
|
+
def export_format(controller)
|
|
105
|
+
{ Mime[:csv].to_s => 'csv', Mime[:json].to_s => 'json' }[controller.response.media_type]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def export?(controller)
|
|
109
|
+
!export_format(controller).nil?
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def tab?(controller, tab)
|
|
113
|
+
!csv?(controller) && controller.params[:tab] == tab
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# A parameter as a String: a scalar, or the first scalar of an
|
|
117
|
+
# array (`?explain_user_id[]=7` — what `find_by(id: [...])` uses),
|
|
118
|
+
# capped; nil for anything else.
|
|
119
|
+
def scalar(value)
|
|
120
|
+
value = value.first if value.is_a?(Array)
|
|
121
|
+
value.to_s[0, VALUE_LIMIT] if value.is_a?(String) || value.is_a?(Numeric)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def id_param(controller, key)
|
|
125
|
+
scalar(controller.params[key])
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def person(controller, data = {})
|
|
129
|
+
{ subject_user_id: id_param(controller, :id), data: data }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def permission_set(controller, data = {})
|
|
133
|
+
{ record: find_set(id_param(controller, :id)), data: data }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def find_set(id)
|
|
137
|
+
PermissionForge::PermissionSet.find_by(id: id) if id
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def filters(controller, names)
|
|
141
|
+
names.to_h { |name| [name, scalar(controller.params[name])] }.compact_blank
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def audit_filters(controller)
|
|
145
|
+
filters(controller, AUDIT_FILTERS)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
requests = Requests
|
|
150
|
+
|
|
151
|
+
# A person's page: effective access tab, audit tab, the CSV of their changes.
|
|
152
|
+
register('permission_forge/people#show', 'audit.exported',
|
|
153
|
+
only_if: requests.method(:csv?),
|
|
154
|
+
payload: ->(controller) { requests.person(controller, 'scope' => 'person', 'format' => 'csv') })
|
|
155
|
+
register('permission_forge/people#show', 'admin.effective_access_viewed',
|
|
156
|
+
only_if: ->(controller) { requests.tab?(controller, 'effective') },
|
|
157
|
+
payload: ->(controller) { requests.person(controller, 'scope' => 'person') })
|
|
158
|
+
register('permission_forge/people#show', 'audit.viewed',
|
|
159
|
+
only_if: ->(controller) { requests.tab?(controller, 'audit') },
|
|
160
|
+
payload: ->(controller) { requests.person(controller, 'scope' => 'person') })
|
|
161
|
+
|
|
162
|
+
# A Permission Set's audit tab and its CSV export.
|
|
163
|
+
register('permission_forge/permission_sets#show', 'audit.exported',
|
|
164
|
+
only_if: requests.method(:csv?),
|
|
165
|
+
payload: lambda { |controller|
|
|
166
|
+
requests.permission_set(controller, 'scope' => 'permission_set', 'format' => 'csv', 'filters' => requests.audit_filters(controller))
|
|
167
|
+
})
|
|
168
|
+
register('permission_forge/permission_sets#show', 'audit.viewed',
|
|
169
|
+
only_if: ->(controller) { requests.tab?(controller, 'audit') },
|
|
170
|
+
payload: lambda { |controller|
|
|
171
|
+
requests.permission_set(controller, 'scope' => 'permission_set', 'filters' => requests.audit_filters(controller))
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
# The field drawer's "explain for a person": that person's effective access to the field.
|
|
175
|
+
# Same test as the controller's (`params[:explain_user_id].present?`), so whatever it explains is recorded.
|
|
176
|
+
register('permission_forge/resource_fields#show', 'admin.effective_access_viewed',
|
|
177
|
+
only_if: ->(controller) { controller.params[:explain_user_id].present? },
|
|
178
|
+
payload: lambda { |controller|
|
|
179
|
+
{ subject_user_id: requests.id_param(controller, :explain_user_id),
|
|
180
|
+
permission_set: requests.find_set(requests.id_param(controller, :permission_set_id)),
|
|
181
|
+
data: { 'scope' => 'field', 'resource' => controller.params[:resource_model].to_s, 'field' => controller.params[:key].to_s } }
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
# The audit log pages (AuditEventsController): the filtered list, its
|
|
185
|
+
# CSV / JSON export, one event, and the "who holds what" export.
|
|
186
|
+
register('permission_forge/audit_events#index', 'audit.exported',
|
|
187
|
+
only_if: requests.method(:export?),
|
|
188
|
+
payload: lambda { |controller|
|
|
189
|
+
{ data: { 'scope' => 'log', 'format' => requests.export_format(controller),
|
|
190
|
+
'filters' => requests.filters(controller, Requests::LOG_FILTERS) } }
|
|
191
|
+
})
|
|
192
|
+
register('permission_forge/audit_events#index', 'audit.viewed',
|
|
193
|
+
payload: lambda { |controller|
|
|
194
|
+
{ data: { 'scope' => 'log', 'filters' => requests.filters(controller, Requests::LOG_FILTERS + %w[views page]) } }
|
|
195
|
+
})
|
|
196
|
+
register('permission_forge/audit_events#show', 'audit.viewed',
|
|
197
|
+
payload: ->(controller) { { data: { 'scope' => 'event', 'identifier' => requests.id_param(controller, :id) } } })
|
|
198
|
+
register('permission_forge/audit_events#holdings', 'audit.exported',
|
|
199
|
+
payload: ->(controller) { { data: { 'scope' => 'holdings', 'format' => requests.export_format(controller) } } })
|
|
200
|
+
|
|
201
|
+
# The JSON API's audit log (Api::AuditEventsController): a page of events, or one.
|
|
202
|
+
register('permission_forge/api/audit_events#index', 'audit.viewed',
|
|
203
|
+
payload: ->(controller) { { data: { 'scope' => 'api', 'filters' => requests.filters(controller, Requests::API_FILTERS) } } })
|
|
204
|
+
register('permission_forge/api/audit_events#show', 'audit.viewed',
|
|
205
|
+
payload: ->(controller) { { data: { 'scope' => 'api', 'identifier' => requests.id_param(controller, :identifier) } } })
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Audit
|
|
5
|
+
# The audit boot warnings, written by the engine once the database is
|
|
6
|
+
# reachable. The missing-table warning is logged by Audit (once per
|
|
7
|
+
# process, by Audit.verify_table!) with the text of .missing_table; this
|
|
8
|
+
# adds auditing switched off and one shared login for every change.
|
|
9
|
+
#
|
|
10
|
+
# @api private
|
|
11
|
+
module BootWarnings
|
|
12
|
+
def self.call(configuration = PermissionForge.configuration)
|
|
13
|
+
PermissionForge::Audit.verify_table!
|
|
14
|
+
|
|
15
|
+
if configuration.audit_events == false
|
|
16
|
+
Rails.logger&.warn '[PermissionForge] SECURITY warning: audit_events is false, so permission changes ' \
|
|
17
|
+
'are not recorded. Set config.audit_events = :auto unless another system records them.'
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
return unless configuration.http_basic_auth_enabled && configuration.audit_actor_resolver.nil?
|
|
21
|
+
|
|
22
|
+
Rails.logger&.warn '[PermissionForge] The admin UI uses HTTP Basic auth and audit_actor_resolver is not ' \
|
|
23
|
+
'set: every change is attributed to the shared login. Set ' \
|
|
24
|
+
'config.audit_actor_resolver to return the signed-in admin.'
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# The missing-table warning (Audit logs it once per process). Its
|
|
28
|
+
# remedy depends on the app: a migration of the gem's that creates the
|
|
29
|
+
# table is there (a fresh install, or an upgrade already generated) and
|
|
30
|
+
# has not run — db:migrate; none — the install predates the audit log,
|
|
31
|
+
# and the upgrade generator writes it (on a fresh install it would
|
|
32
|
+
# duplicate migrations).
|
|
33
|
+
#
|
|
34
|
+
# @param table [String] the audit events table name
|
|
35
|
+
# @return [String]
|
|
36
|
+
def self.missing_table(table)
|
|
37
|
+
remedy = if audit_migration_present?
|
|
38
|
+
'the pending migration has run (bin/rails db:migrate)'
|
|
39
|
+
else
|
|
40
|
+
'its migration is generated and run (bin/rails generate permission_forge:upgrade, then bin/rails db:migrate)'
|
|
41
|
+
end
|
|
42
|
+
"[PermissionForge] audit_events is :auto but the table '#{table}' was not found: no audit events are " \
|
|
43
|
+
"recorded until #{remedy}. Changes still work."
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @return [Boolean] whether a permission_forge migration in the app's
|
|
47
|
+
# migration paths creates the audit events table
|
|
48
|
+
def self.audit_migration_present?
|
|
49
|
+
Array(ActiveRecord::Migrator.migrations_paths).any? do |path|
|
|
50
|
+
Dir.glob(File.join(path.to_s, '*permission_forge*.rb')).any? { |file| File.read(file).include?('audit_events') }
|
|
51
|
+
end
|
|
52
|
+
rescue SystemCallError
|
|
53
|
+
false
|
|
54
|
+
end
|
|
55
|
+
private_class_method :audit_migration_present?
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Audit
|
|
5
|
+
# The one cleaning of every string an event stores in a column of its own
|
|
6
|
+
# — whoever supplied it: a request (reason, a typed Basic username, a
|
|
7
|
+
# request id), the host (a resolver's label, with_audit_actor, a rake
|
|
8
|
+
# ACTOR or REASON) or the gem. Audit::Recorder applies it to every event
|
|
9
|
+
# it builds.
|
|
10
|
+
#
|
|
11
|
+
# - valid UTF-8 (invalid bytes replaced), so a row is never refused
|
|
12
|
+
# - one line: tab, newline and the other line breaks become a space;
|
|
13
|
+
# NUL (PostgreSQL refuses it) and every other C0/C1 control character
|
|
14
|
+
# (terminal escapes) are removed
|
|
15
|
+
# - at most LIMIT characters: the columns are varchar(255), and MySQL
|
|
16
|
+
# in strict mode refuses — not truncates — a longer value, which
|
|
17
|
+
# would let a client keep its own refusal out of the log
|
|
18
|
+
#
|
|
19
|
+
# `data` (JSON, escaped by the encoder) is left as it is.
|
|
20
|
+
#
|
|
21
|
+
# @api private
|
|
22
|
+
module ColumnValues
|
|
23
|
+
LIMIT = 255
|
|
24
|
+
COLUMNS = %i[event category actor_type actor_id actor_label source record_type record_id record_uuid record_label
|
|
25
|
+
permission_set_uuid subject_user_id reason request_id ip correlation_id].freeze
|
|
26
|
+
LINE_BREAKS = /[\t\n\v\f\r\u0085
]/
|
|
27
|
+
CONTROLS = /[\u0000-\u001F\u007F-\u009F]/
|
|
28
|
+
|
|
29
|
+
class << self
|
|
30
|
+
# @param row [Hash] the event's attributes
|
|
31
|
+
# @return [Hash] a copy with every String in COLUMNS cleaned
|
|
32
|
+
def clean(row)
|
|
33
|
+
row.to_h { |column, value| [column, COLUMNS.include?(column) ? value(value) : value] }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @return [String, Object] a cleaned String; anything else unchanged
|
|
37
|
+
def value(value)
|
|
38
|
+
return value unless value.is_a?(String)
|
|
39
|
+
|
|
40
|
+
utf8(value).gsub(LINE_BREAKS, ' ').gsub(CONTROLS, '')[0, LIMIT]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def utf8(value)
|
|
46
|
+
text = if [Encoding::UTF_8, Encoding::BINARY].include?(value.encoding)
|
|
47
|
+
value.dup.force_encoding(Encoding::UTF_8)
|
|
48
|
+
else
|
|
49
|
+
value.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
|
|
50
|
+
end
|
|
51
|
+
text.scrub
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|