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,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'value_casters/number'
|
|
4
|
+
require_relative 'value_casters/temporal'
|
|
5
|
+
|
|
6
|
+
module PermissionForge
|
|
7
|
+
module Operators
|
|
8
|
+
# What a rule value becomes for a column of each ActiveRecord type: a Ruby
|
|
9
|
+
# value the adapter quotes as that type, or {UNUSABLE} when the text is not
|
|
10
|
+
# one of that type. Each caster is a `#call(value)`; {.for} picks the one
|
|
11
|
+
# for a column.
|
|
12
|
+
#
|
|
13
|
+
# The notations are the same on every database and deliberately narrower
|
|
14
|
+
# than Ruby's own parsers, which take `4_2`, `0x5` and `NaN`.
|
|
15
|
+
module ValueCasters
|
|
16
|
+
# A value the column cannot hold. A rule with one matches nothing.
|
|
17
|
+
UNUSABLE = Object.new.tap { |marker| marker.define_singleton_method(:inspect) { 'UNUSABLE' } }.freeze
|
|
18
|
+
|
|
19
|
+
# What PostgreSQL's boolean input takes. The id `current_user` stands for
|
|
20
|
+
# is not text, and so never a boolean.
|
|
21
|
+
BOOLEANS = {
|
|
22
|
+
true => true, 'true' => true, 't' => true, 'yes' => true, 'y' => true, 'on' => true, '1' => true,
|
|
23
|
+
false => false, 'false' => false, 'f' => false, 'no' => false, 'n' => false, 'off' => false, '0' => false
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
NUMBER = Number::EXACT
|
|
27
|
+
|
|
28
|
+
BOOLEAN = ->(value) { BOOLEANS.fetch(value.is_a?(String) ? value.strip.downcase : value, UNUSABLE) }
|
|
29
|
+
|
|
30
|
+
# Compared as written; the id `current_user` stands for, as its text.
|
|
31
|
+
TEXT = ->(value) { value.nil? ? UNUSABLE : value.to_s }
|
|
32
|
+
|
|
33
|
+
# A column type without a caster (uuid, json, an array column, ...) or a
|
|
34
|
+
# field name that names no known column: the value is left to the database.
|
|
35
|
+
UNTYPED = ->(value) { value }
|
|
36
|
+
|
|
37
|
+
BY_TYPE = {
|
|
38
|
+
integer: NUMBER, float: NUMBER, decimal: NUMBER,
|
|
39
|
+
boolean: BOOLEAN,
|
|
40
|
+
string: TEXT, text: TEXT,
|
|
41
|
+
date: Temporal::DATE, time: Temporal::TIME,
|
|
42
|
+
datetime: Temporal::DATETIME, timestamp: Temporal::DATETIME, timestamptz: Temporal::DATETIME
|
|
43
|
+
}.freeze
|
|
44
|
+
|
|
45
|
+
# PostgreSQL is typed like the others, with two things left the way it
|
|
46
|
+
# always did them, so that no rule that works there stops working:
|
|
47
|
+
#
|
|
48
|
+
# - It reads date and time text itself and raises on what it cannot
|
|
49
|
+
# read. Text the Temporal casters do not recognise is left to it
|
|
50
|
+
# (`due_on less_than '31 Jan 2026'`, `due_on equal 'today'`); on SQLite
|
|
51
|
+
# and MySQL, which compare or convert such text, it stays UNUSABLE.
|
|
52
|
+
# - A `real` column is compared in single precision (Number::SINGLE_PRECISION).
|
|
53
|
+
# The SQL type is the key: its ActiveRecord type is `float` as well.
|
|
54
|
+
POSTGRESQL = BY_TYPE.transform_values { |caster| caster.is_a?(Temporal) ? caster.as_written : caster }
|
|
55
|
+
.merge('real' => Number::SINGLE_PRECISION).freeze
|
|
56
|
+
|
|
57
|
+
# Adapter name => the casters of that database, where they are not BY_TYPE.
|
|
58
|
+
BY_ADAPTER = { 'PostgreSQL' => POSTGRESQL, 'PostGIS' => POSTGRESQL }.freeze
|
|
59
|
+
|
|
60
|
+
# The casters that read the two bounds of `between` together, where the
|
|
61
|
+
# others cast one bound at a time. With nothing known about the column
|
|
62
|
+
# the pair is read as it was before values were typed: both integers,
|
|
63
|
+
# both decimals, both dates, or text.
|
|
64
|
+
BOUNDS = {
|
|
65
|
+
UNTYPED => ->(bounds) { Registry.coerce_between_values(*bounds) },
|
|
66
|
+
Number::SINGLE_PRECISION => Number::SINGLE_PRECISION_BOUNDS
|
|
67
|
+
}.freeze
|
|
68
|
+
|
|
69
|
+
# @param column [ActiveRecord::ConnectionAdapters::Column, nil]
|
|
70
|
+
# @param adapter_name [String, nil] the adapter of the column's model
|
|
71
|
+
# @return [#call] the caster for the column
|
|
72
|
+
def self.for(column, adapter_name = nil)
|
|
73
|
+
return UNTYPED if column.nil? || column.try(:array?)
|
|
74
|
+
|
|
75
|
+
casters = BY_ADAPTER.fetch(adapter_name, BY_TYPE)
|
|
76
|
+
casters.fetch(column.sql_type) { casters.fetch(column.type, UNTYPED) }
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Operators
|
|
5
|
+
# Gives a rule value the type of the column it is compared with, so no
|
|
6
|
+
# database is left to convert one side of a comparison.
|
|
7
|
+
#
|
|
8
|
+
# Rule values are text, and databases disagree about text compared with a
|
|
9
|
+
# number, a boolean or a date: PostgreSQL raises on text it cannot cast,
|
|
10
|
+
# SQLite compares as text, and MySQL converts — there `5 = '5_id'` and
|
|
11
|
+
# `0 = 'abc'` are true, and a boolean column equals `'true'` exactly when
|
|
12
|
+
# it is false. So the value is typed here, the same way for every database
|
|
13
|
+
# and for the RLS policy (Services::RlsOperatorSql):
|
|
14
|
+
#
|
|
15
|
+
# - a value the column can hold is bound as a value of its type
|
|
16
|
+
# ({ValueCasters}); an enum label, as the value the model stores for it;
|
|
17
|
+
# - a value it cannot hold is {UNUSABLE}: the rule matches nothing.
|
|
18
|
+
#
|
|
19
|
+
# PostgreSQL, which raises instead of guessing, keeps two habits of its own
|
|
20
|
+
# (ValueCasters::POSTGRESQL): it reads date and time text in notations of
|
|
21
|
+
# its own, and compares a `real` column in single precision.
|
|
22
|
+
#
|
|
23
|
+
# Only the built-in operators are typed, each by the shape of its value
|
|
24
|
+
# ({SHAPES}). A custom operator's value is returned as it came, and a
|
|
25
|
+
# column nothing is known about leaves the value to the database. A rule
|
|
26
|
+
# whose field_name is not a name (RuleColumn#malformed?) has no column to
|
|
27
|
+
# compare with: its value is UNUSABLE whatever the operator.
|
|
28
|
+
module ValueTyping
|
|
29
|
+
UNUSABLE = ValueCasters::UNUSABLE
|
|
30
|
+
CURRENT_USER = 'current_user'
|
|
31
|
+
|
|
32
|
+
# The shape of each built-in operator's value. The null checks take none.
|
|
33
|
+
SHAPES = {
|
|
34
|
+
Constants::EQUAL => :single, Constants::NOT_EQUAL => :single,
|
|
35
|
+
Constants::LESS_THAN => :single, Constants::GREATER_THAN => :single,
|
|
36
|
+
Constants::LESS_THAN_OR_EQUAL => :single, Constants::GREATER_THAN_OR_EQUAL => :single,
|
|
37
|
+
Constants::IN => :list, Constants::NOT_IN => :list, Constants::BETWEEN => :pair,
|
|
38
|
+
Constants::CONTAINS => :pattern, Constants::NOT_CONTAINS => :pattern,
|
|
39
|
+
Constants::STARTS_WITH => :pattern, Constants::ENDS_WITH => :pattern
|
|
40
|
+
}.freeze
|
|
41
|
+
# The shapes read from the rule's text as a whole, comma by comma.
|
|
42
|
+
SEVERAL = %i[list pair].freeze
|
|
43
|
+
|
|
44
|
+
module_function
|
|
45
|
+
|
|
46
|
+
# @param operator [String] the rule's operator
|
|
47
|
+
# @param value [Object] the rule's value — the user's id where
|
|
48
|
+
# `current_user` stood for it (see {.current_user?})
|
|
49
|
+
# @param target [RuleColumn] the column it is compared with
|
|
50
|
+
# @return [Object] what the operator's strategy binds: one value, an Array
|
|
51
|
+
# for `in` / `not_in` / `between`, or {UNUSABLE}
|
|
52
|
+
def call(operator, value, target)
|
|
53
|
+
return UNUSABLE if target.malformed?
|
|
54
|
+
|
|
55
|
+
shape = SHAPES[operator]
|
|
56
|
+
shape ? send(shape, value, target) : value
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# `current_user` stands for the user's id wherever one value is compared.
|
|
60
|
+
# In a list it is an element like any other.
|
|
61
|
+
def current_user?(operator, value)
|
|
62
|
+
value == CURRENT_USER && SEVERAL.exclude?(SHAPES[operator])
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# The caster for the column's type; an enum label is first replaced by
|
|
66
|
+
# the value the model stores for it.
|
|
67
|
+
#
|
|
68
|
+
# @return [#call]
|
|
69
|
+
def caster(target)
|
|
70
|
+
typed = target.caster
|
|
71
|
+
labels = target.enum
|
|
72
|
+
return typed unless labels
|
|
73
|
+
|
|
74
|
+
->(value) { typed.call(labels.fetch(value.is_a?(String) ? value.strip : value, value)) }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def single(value, target)
|
|
78
|
+
caster(target).call(value)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# A LIKE pattern is built from the text of the value, whatever the column.
|
|
82
|
+
def pattern(value, _target)
|
|
83
|
+
ValueCasters::TEXT.call(value)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Every element has to be a value of the column; a list of none is no list.
|
|
87
|
+
def list(value, target)
|
|
88
|
+
elements = split(value)
|
|
89
|
+
elements.empty? ? UNUSABLE : all_typed(elements, caster(target))
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Exactly two bounds, each a value of the column — or read together, by
|
|
93
|
+
# the casters that do so (ValueCasters::BOUNDS).
|
|
94
|
+
def pair(value, target)
|
|
95
|
+
bounds = split(value)
|
|
96
|
+
return UNUSABLE unless bounds.size == 2
|
|
97
|
+
|
|
98
|
+
typed = caster(target)
|
|
99
|
+
together = ValueCasters::BOUNDS[typed]
|
|
100
|
+
together ? together.call(bounds) : all_typed(bounds, typed)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def split(value)
|
|
104
|
+
value.to_s.split(',').map(&:strip)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def all_typed(elements, caster)
|
|
108
|
+
typed = elements.map { |element| caster.call(element) }
|
|
109
|
+
typed.include?(UNUSABLE) ? UNUSABLE : typed
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private_class_method :caster, :single, :pattern, :list, :pair, :split, :all_typed
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
# Boot warnings for the three query-scoping levels (called by BootChecks).
|
|
5
|
+
#
|
|
6
|
+
# @api private
|
|
7
|
+
module QueryScopingWarnings
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
# Level 2 in :raise mode is a dev/test/CI guard; raising in production
|
|
11
|
+
# turns a missed call site into an outage. Allowed, but never silently.
|
|
12
|
+
def warn_unscoped_detection_raise_in_production(pf_config)
|
|
13
|
+
return unless pf_config.unscoped_query_detection == :raise
|
|
14
|
+
return unless pf_config.unscoped_query_detection_environments.include?('production')
|
|
15
|
+
|
|
16
|
+
Rails.logger&.warn '[PermissionForge] WARNING: unscoped_query_detection = :raise is enabled ' \
|
|
17
|
+
'for production. A single unscoped query will raise UnscopedQueryError ' \
|
|
18
|
+
'in live traffic — prefer :log in production and :raise in test/CI.'
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Level 1 applies filter-type rules only; join-type rules still require
|
|
22
|
+
# permission_scoped. Surface that gap when both are in play.
|
|
23
|
+
def warn_join_rules_not_auto_scoped(pf_config)
|
|
24
|
+
return unless pf_config.auto_query_scoping_enabled?
|
|
25
|
+
|
|
26
|
+
join_rules = PermissionForge::PermissionSetRule.join_type_rules
|
|
27
|
+
return unless join_rules.exists?
|
|
28
|
+
|
|
29
|
+
Rails.logger&.warn '[PermissionForge] SECURITY warning: auto_query_scoping is enabled but ' \
|
|
30
|
+
"#{join_rules.count} join-type rule(s) exist. Join rules are NOT applied " \
|
|
31
|
+
'by auto-scoping — queries relying on them must go through ' \
|
|
32
|
+
'Model.permission_scoped.'
|
|
33
|
+
rescue ActiveRecord::StatementInvalid
|
|
34
|
+
# Tables don't exist yet (pre-migration)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Level 3 needs Postgres; on any other adapter the session variable and
|
|
38
|
+
# policies simply don't exist, so enabling it is a silent no-op — warn.
|
|
39
|
+
# On Postgres, compare live policy digests against the current rule state
|
|
40
|
+
# so edited rules can't silently diverge from the installed policies.
|
|
41
|
+
def warn_rls_misconfiguration(pf_config)
|
|
42
|
+
return unless pf_config.database_row_level_security
|
|
43
|
+
|
|
44
|
+
unless PermissionForge::RLS.postgres?
|
|
45
|
+
Rails.logger&.warn '[PermissionForge] WARNING: database_row_level_security is enabled but ' \
|
|
46
|
+
'the database adapter is not PostgreSQL. RLS policies cannot be enforced — ' \
|
|
47
|
+
'row-level security falls back to application-level scoping only.'
|
|
48
|
+
return
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
warn_rls_policy_drift
|
|
52
|
+
rescue ActiveRecord::NoDatabaseError, ActiveRecord::ConnectionNotEstablished
|
|
53
|
+
# Pre-boot: adapter unknown
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def warn_rls_policy_drift
|
|
57
|
+
stale = PermissionForge::RLS.drift_report.reject { |report| report[:in_sync] }
|
|
58
|
+
return if stale.empty?
|
|
59
|
+
|
|
60
|
+
Rails.logger&.warn '[PermissionForge] SECURITY warning: RLS policies are missing or stale for ' \
|
|
61
|
+
"table(s): #{stale.map { |report| report[:table] }.join(', ')}. " \
|
|
62
|
+
'Rule rows have changed since the last policy sync (or policies were never ' \
|
|
63
|
+
'installed) — run `rake permission_forge:rls:sync`.'
|
|
64
|
+
rescue ActiveRecord::StatementInvalid
|
|
65
|
+
# Tables don't exist yet (pre-migration)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
# What every rake task of the gem does about the audit log: names who ran it
|
|
5
|
+
# (ACTOR=, stored as actor_label with actor_type 'system' and source 'rake'),
|
|
6
|
+
# records `report.generated` for the read-only ones, and logs through
|
|
7
|
+
# Rails.logger. The tasks' own output is untouched.
|
|
8
|
+
#
|
|
9
|
+
# PermissionForge::RakeAudit.changing('permission_forge:erasure') { ... } # warns without ACTOR
|
|
10
|
+
# PermissionForge::RakeAudit.reporting('permission_forge:audit', counts: ->(r) { r.counts }) { report }
|
|
11
|
+
#
|
|
12
|
+
# @api private
|
|
13
|
+
module RakeAudit
|
|
14
|
+
class << self
|
|
15
|
+
# A task that changes data. Without ACTOR the label is "rake <task>" and
|
|
16
|
+
# a warning is printed.
|
|
17
|
+
def changing(task, env: ENV, warn_io: $stderr, &)
|
|
18
|
+
warn_io.puts(missing_actor_warning(task)) if env['ACTOR'].blank? && warn_io
|
|
19
|
+
run(task, env, &)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# A read-only task: runs the block (it returns the task's result), records
|
|
23
|
+
# `report.generated` (task name, counts of that result) and returns the
|
|
24
|
+
# result — before the task prints or exits.
|
|
25
|
+
def reporting(task, counts:, env: ENV)
|
|
26
|
+
run(task, env) do
|
|
27
|
+
result = yield
|
|
28
|
+
record_report(task, counts.call(result))
|
|
29
|
+
result
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def label(task, env = ENV)
|
|
34
|
+
env['ACTOR'].presence || "rake #{task}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# One line in the application log.
|
|
38
|
+
def log(task, message)
|
|
39
|
+
Rails.logger&.info("[permission_forge] #{task}: #{message}")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
# A report must never fail because its own event cannot be written: the
|
|
45
|
+
# failure is reported (Rails.error, the log) and the task goes on.
|
|
46
|
+
def record_report(task, counts)
|
|
47
|
+
PermissionForge::Audit.record('report.generated', data: { 'task' => task, 'counts' => counts.as_json })
|
|
48
|
+
rescue StandardError => e
|
|
49
|
+
PermissionForge::Audit::Failure.handle(e, strategy: :report, operation: :store, event: 'report.generated', task: task)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def run(task, env, &)
|
|
53
|
+
actor = label(task, env)
|
|
54
|
+
log(task, "run by #{actor}")
|
|
55
|
+
PermissionForge::Audit::Context.with(
|
|
56
|
+
actor: PermissionForge::Audit::Actor.system(actor), source: 'rake', reason: env['REASON'].presence, &
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def missing_actor_warning(task)
|
|
61
|
+
"WARNING: ACTOR is not set; recorded as \"rake #{task}\". Usage: rake #{task} ACTOR=\"your name or ticket\""
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
# The Resource names a model class answers to: its own, and those of its
|
|
5
|
+
# single-table-inheritance ancestors up to base_class — nearest first.
|
|
6
|
+
# `VipCustomer < Customer` gives [:VipCustomer, :Customer]; a plain model
|
|
7
|
+
# its own name alone.
|
|
8
|
+
#
|
|
9
|
+
# Every entry the user has along the chain counts, and a subclass can only
|
|
10
|
+
# be narrower than its base: the rules of every entry apply together; a
|
|
11
|
+
# view_all / modify_all bypass skips the rules of the entry that grants it
|
|
12
|
+
# and no other; a field is as closed as the strictest entry that configures
|
|
13
|
+
# it, and an entry that does not grant read hides every field the chain
|
|
14
|
+
# configures. A subclass with no entry of its own is its base. (A class
|
|
15
|
+
# used to answer to its own name only, so a subclass of a protected model
|
|
16
|
+
# had no rules at all and every field of it was unconfigured, before
|
|
17
|
+
# 0.14.) A subclass with a table of its own is not
|
|
18
|
+
# STI and answers to its own name alone.
|
|
19
|
+
#
|
|
20
|
+
# The one resolver for every path: find_by / permission_scoped
|
|
21
|
+
# (PermissionScopedFindable), the auto scope (AutoPermissionScoping), the
|
|
22
|
+
# attribute guard (MaskedAttributes, AttributeAccessGuard), the response
|
|
23
|
+
# filter (FiltersResponseAttributes) and PermitsAttributes.
|
|
24
|
+
#
|
|
25
|
+
# @api private
|
|
26
|
+
module ResourceChain
|
|
27
|
+
# The entry of a field hidden by an unreadable entry along the chain.
|
|
28
|
+
HIDDEN_ENTRY = { read_access: false, edit_access: false, mask_access: false, mask_strategy: 'full' }.freeze
|
|
29
|
+
|
|
30
|
+
module_function
|
|
31
|
+
|
|
32
|
+
# @param klass [Class] a model class (any class answers to its own name)
|
|
33
|
+
# @return [Array<Symbol>] frozen; worked out once per class
|
|
34
|
+
def names(klass)
|
|
35
|
+
klass.instance_variable_get(:@pf_resource_chain) ||
|
|
36
|
+
klass.instance_variable_set(:@pf_resource_chain, compute_names(klass))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The names behind a resource name: the chain of the model it names, or
|
|
40
|
+
# the name alone when it names no model.
|
|
41
|
+
#
|
|
42
|
+
# @param resource_name [String, Symbol]
|
|
43
|
+
# @return [Array<Symbol>]
|
|
44
|
+
def names_for(resource_name)
|
|
45
|
+
klass = resource_name.to_s.safe_constantize
|
|
46
|
+
klass.is_a?(Class) && klass.respond_to?(:base_class) ? names(klass) : [resource_name.to_sym]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# The user's resource entries along the chain, nearest first.
|
|
50
|
+
#
|
|
51
|
+
# @param user [#permissions_hash]
|
|
52
|
+
# @param names [Array<Symbol>]
|
|
53
|
+
# @return [Array<Hash>]
|
|
54
|
+
def entries(user, names)
|
|
55
|
+
resources = user.permissions_hash.dig(:permissions, :resources)
|
|
56
|
+
return [] unless resources
|
|
57
|
+
|
|
58
|
+
names.filter_map do |name|
|
|
59
|
+
entry = resources[name]
|
|
60
|
+
entry if entry.present?
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The row-level rules that apply to a class, and why none do.
|
|
65
|
+
#
|
|
66
|
+
# @return [Array(Array<Hash>, Symbol)] the rules of every entry without a
|
|
67
|
+
# bypass, and — when that leaves none — the nearest entry's :view_all /
|
|
68
|
+
# :modify_all, else :no_rules
|
|
69
|
+
def rules(user, names)
|
|
70
|
+
rules = []
|
|
71
|
+
bypass = nil
|
|
72
|
+
entries(user, names).each do |entry|
|
|
73
|
+
reason = bypass_reason(entry[:permissions])
|
|
74
|
+
if reason
|
|
75
|
+
bypass ||= reason
|
|
76
|
+
else
|
|
77
|
+
rules.concat(entry[:permission_set_rules] || [])
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
[rules, (bypass || :no_rules if rules.empty?)]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Whether the entry the class answers to grants a resource flag: the
|
|
84
|
+
# nearest entry along the chain — the class's own, or its base's when it
|
|
85
|
+
# has none of its own (the same entry can_*? reads for a class with one;
|
|
86
|
+
# a muted base reaches it, see PermissionHashBuilder's muting phase).
|
|
87
|
+
# False without an entry.
|
|
88
|
+
#
|
|
89
|
+
# @param flag [Symbol] :read_access, :delete_access, …
|
|
90
|
+
# @return [Boolean]
|
|
91
|
+
def grants?(user, names, flag)
|
|
92
|
+
entries(user, names).first&.dig(:permissions, flag) == true
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# @return [Symbol, nil] :view_all or :modify_all when one of them lets
|
|
96
|
+
# the user past the rules of an entry
|
|
97
|
+
def bypass_reason(permissions)
|
|
98
|
+
return unless permissions
|
|
99
|
+
|
|
100
|
+
if permissions[:view_all] then :view_all
|
|
101
|
+
elsif permissions[:modify_all] then :modify_all
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# The field entry a class answers with for one attribute: the one entry
|
|
106
|
+
# along the chain that configures it — one entry is read as a plain
|
|
107
|
+
# model's, its rows decide — or, among several, the strictest; hidden
|
|
108
|
+
# when one of them does not grant read; nil when none configures it (or
|
|
109
|
+
# the user has no field_permissions).
|
|
110
|
+
#
|
|
111
|
+
# @param names [Array<Symbol>, Symbol] the chain, or one name
|
|
112
|
+
# @return [Hash, nil]
|
|
113
|
+
def field_permissions(user, names, attribute)
|
|
114
|
+
return unless user.respond_to?(:field_permissions)
|
|
115
|
+
return user.field_permissions(names, attribute) unless names.is_a?(Array)
|
|
116
|
+
return user.field_permissions(names.first, attribute) if names.size == 1
|
|
117
|
+
return strictest(names.filter_map { |name| user.field_permissions(name, attribute) }) unless user.respond_to?(:permissions_hash)
|
|
118
|
+
|
|
119
|
+
compose_field(entries(user, names), attribute.to_sym)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# One entry: its field entry (its rows decide). Several: hidden when one
|
|
123
|
+
# does not grant read, else the strictest of those that configure the
|
|
124
|
+
# field; nil when none does.
|
|
125
|
+
def compose_field(resource_entries, attribute)
|
|
126
|
+
field_entries = resource_entries.filter_map { |entry| entry.dig(:fields, attribute) }
|
|
127
|
+
return field_entries.first if resource_entries.size <= 1 || field_entries.empty?
|
|
128
|
+
|
|
129
|
+
unreadable?(resource_entries) ? HIDDEN_ENTRY.dup : strictest(field_entries)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# The field entries a class answers with, every configured name composed
|
|
133
|
+
# the same way. The one table itself when one entry has fields: read-only.
|
|
134
|
+
#
|
|
135
|
+
# @return [Hash{Symbol => Hash}]
|
|
136
|
+
def fields(user, names)
|
|
137
|
+
resource_entries = entries(user, names)
|
|
138
|
+
tables = resource_entries.filter_map { |entry| entry[:fields].presence }
|
|
139
|
+
return tables.first || {} if resource_entries.size <= 1
|
|
140
|
+
|
|
141
|
+
compose_tables(tables, unreadable?(resource_entries))
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def compose_tables(tables, hidden)
|
|
145
|
+
tables.flat_map(&:keys).uniq.to_h do |name|
|
|
146
|
+
[name, hidden ? HIDDEN_ENTRY.dup : strictest(tables.filter_map { |table| table[name] })]
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Whether one of several entries does not grant read: a class cannot be
|
|
151
|
+
# more readable than an entry it answers to (a muted base read reaches
|
|
152
|
+
# the fields only the subclass configures).
|
|
153
|
+
def unreadable?(resource_entries)
|
|
154
|
+
resource_entries.any? { |entry| entry.dig(:permissions, :read_access) != true }
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Of several entries for one field: readable and editable only when every
|
|
158
|
+
# one grants it, masked when any does, with the first masking entry's
|
|
159
|
+
# strategy (in chain order). One entry: itself.
|
|
160
|
+
def strictest(field_entries)
|
|
161
|
+
return field_entries.first if field_entries.size <= 1
|
|
162
|
+
|
|
163
|
+
masked = field_entries.find { |entry| entry[:mask_access] }
|
|
164
|
+
{ read_access: field_entries.all? { |entry| entry[:read_access] == true },
|
|
165
|
+
edit_access: field_entries.all? { |entry| entry[:edit_access] == true },
|
|
166
|
+
mask_access: !masked.nil?,
|
|
167
|
+
mask_strategy: (masked || field_entries.first)[:mask_strategy] }
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# The class and its superclasses up to base_class that share its table
|
|
171
|
+
# (single-table inheritance: Rails' base_class ignores table_name, and a
|
|
172
|
+
# subclass with a table of its own holds other rows); the names they
|
|
173
|
+
# have. A nameless class (an anonymous subclass) answers as its ancestors
|
|
174
|
+
# do; a subclass renamed after its base answers once.
|
|
175
|
+
def compute_names(klass)
|
|
176
|
+
classes = [klass]
|
|
177
|
+
if klass.respond_to?(:base_class)
|
|
178
|
+
base = klass.base_class
|
|
179
|
+
classes << classes.last.superclass while classes.last != base && same_table?(classes.last.superclass, klass)
|
|
180
|
+
end
|
|
181
|
+
classes.filter_map(&:name).map(&:to_sym).uniq.freeze
|
|
182
|
+
end
|
|
183
|
+
private_class_method :compute_names
|
|
184
|
+
|
|
185
|
+
def same_table?(ancestor, klass)
|
|
186
|
+
ancestor.respond_to?(:table_name) && ancestor.table_name == klass.table_name
|
|
187
|
+
end
|
|
188
|
+
private_class_method :same_table?
|
|
189
|
+
end
|
|
190
|
+
end
|