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,116 @@
|
|
|
1
|
+
module PermissionForge
|
|
2
|
+
module Concerns
|
|
3
|
+
# CRUD permission checks, generic action dispatch, and resource-level hash accessors.
|
|
4
|
+
# Depends on PermissionCacheAccessor for shared helpers.
|
|
5
|
+
#
|
|
6
|
+
# @api public
|
|
7
|
+
module ResourcePermissionChecker
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
# The words can? answers from the resource's CRUD flags, and the
|
|
11
|
+
# permission each one means. Anything else is a custom action.
|
|
12
|
+
# ControllerActions reads this table too, so the guard and can? always
|
|
13
|
+
# agree on which keys are CRUD.
|
|
14
|
+
CRUD_ACTIONS = { read: :read, create: :create, edit: :edit, update: :edit, delete: :delete, destroy: :delete }.freeze
|
|
15
|
+
CRUD_PREDICATES = { read: :can_read?, create: :can_create?, edit: :can_edit?, delete: :can_delete? }.freeze
|
|
16
|
+
|
|
17
|
+
# @param resource [Symbol, String] resource name (e.g., :Customer)
|
|
18
|
+
# @return [Boolean]
|
|
19
|
+
def can_read?(resource)
|
|
20
|
+
pf_check_resource_permission(resource, :read_access)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
alias can_show? can_read?
|
|
24
|
+
alias has_read_access? can_read?
|
|
25
|
+
|
|
26
|
+
# @param resource [Symbol, String] resource name
|
|
27
|
+
# @return [Boolean]
|
|
28
|
+
def can_create?(resource)
|
|
29
|
+
pf_check_resource_permission(resource, :create_access)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
alias can_create_new? can_create?
|
|
33
|
+
alias has_create_access? can_create?
|
|
34
|
+
|
|
35
|
+
# @param resource [Symbol, String] resource name
|
|
36
|
+
# @return [Boolean]
|
|
37
|
+
def can_edit?(resource)
|
|
38
|
+
pf_check_resource_permission(resource, :edit_access)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
alias can_update? can_edit?
|
|
42
|
+
alias has_edit_access? can_edit?
|
|
43
|
+
|
|
44
|
+
# @param resource [Symbol, String] resource name
|
|
45
|
+
# @return [Boolean]
|
|
46
|
+
def can_delete?(resource)
|
|
47
|
+
pf_check_resource_permission(resource, :delete_access)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
alias can_destroy? can_delete?
|
|
51
|
+
alias has_delete_access? can_delete?
|
|
52
|
+
|
|
53
|
+
# @param resource [Symbol, String] resource name
|
|
54
|
+
# @return [Boolean]
|
|
55
|
+
def can_view_all?(resource)
|
|
56
|
+
pf_check_resource_permission(resource, :view_all)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @param resource [Symbol, String] resource name
|
|
60
|
+
# @return [Boolean]
|
|
61
|
+
def can_modify_all?(resource)
|
|
62
|
+
pf_check_resource_permission(resource, :modify_all)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Returns the resource-level permissions hash (read_access, create_access, etc.).
|
|
66
|
+
#
|
|
67
|
+
# @param resource [Symbol, String] resource name
|
|
68
|
+
# @return [Hash] permission flags, or empty hash if resource not found
|
|
69
|
+
def permissions_for(resource)
|
|
70
|
+
perms = pf_resource_permissions(resource)
|
|
71
|
+
return {} if perms.blank?
|
|
72
|
+
|
|
73
|
+
perms
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Returns the full resource entry including permissions, fields, actions, and rules.
|
|
77
|
+
#
|
|
78
|
+
# @param resource [Symbol, String] resource name
|
|
79
|
+
# @return [Hash] full resource entry, or empty hash if not found
|
|
80
|
+
def permissions_hash_for(resource)
|
|
81
|
+
entry = pf_resource_name_permissions(resource)
|
|
82
|
+
return {} if entry.blank?
|
|
83
|
+
|
|
84
|
+
entry
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Checks if the user can perform an action on a resource.
|
|
88
|
+
# Delegates CRUD actions to specific helpers; custom actions check resource_actions.
|
|
89
|
+
#
|
|
90
|
+
# @param action [Symbol, String] action name (:read, :create, :edit, :delete, or custom)
|
|
91
|
+
# @param resource [Symbol, String] resource name
|
|
92
|
+
# @return [Boolean]
|
|
93
|
+
def can?(action, resource)
|
|
94
|
+
action = action.to_sym
|
|
95
|
+
resource = resource.to_s.to_sym unless resource.is_a?(Symbol)
|
|
96
|
+
|
|
97
|
+
crud = CRUD_ACTIONS[action]
|
|
98
|
+
return public_send(CRUD_PREDICATES.fetch(crud), resource) if crud
|
|
99
|
+
|
|
100
|
+
resource_hash = pf_resource_name_permissions(resource)
|
|
101
|
+
return false unless resource_hash
|
|
102
|
+
|
|
103
|
+
resource_hash.dig(:actions, action, :is_permitted) == true
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
def pf_check_resource_permission(resource, action)
|
|
109
|
+
perms = pf_resource_permissions(resource)
|
|
110
|
+
return false if perms.blank?
|
|
111
|
+
|
|
112
|
+
perms[action]
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
module PermissionForge
|
|
2
|
+
module Concerns
|
|
3
|
+
# Refuses a string the database cannot hold as a validation error, so the
|
|
4
|
+
# admin and the API answer 422 where a strict MySQL raised ValueTooLong
|
|
5
|
+
# (a 500). The limit is the column's own, read from the schema: a
|
|
6
|
+
# `string` column's in characters, a `text` column's in bytes (MySQL
|
|
7
|
+
# TEXT holds 65,535 of them). A column that declares no limit (a
|
|
8
|
+
# PostgreSQL or SQLite `string`) is not checked, and an attribute with a
|
|
9
|
+
# length rule of its own at most as long as the column (PermissionSet#name)
|
|
10
|
+
# keeps that rule alone.
|
|
11
|
+
#
|
|
12
|
+
# Only attributes being written are checked: a longer value stored
|
|
13
|
+
# before (on a database that held it) leaves the row's other attributes
|
|
14
|
+
# editable.
|
|
15
|
+
#
|
|
16
|
+
# @api private
|
|
17
|
+
module ValidatesColumnLengths
|
|
18
|
+
extend ActiveSupport::Concern
|
|
19
|
+
|
|
20
|
+
# How each column type measures its limit.
|
|
21
|
+
MEASURES = {
|
|
22
|
+
string: :length,
|
|
23
|
+
text: :bytesize
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
included do
|
|
27
|
+
validate :validate_column_lengths
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The longest value a validator lets through; any other validator
|
|
31
|
+
# lets through anything.
|
|
32
|
+
def self.maximum_of(validator)
|
|
33
|
+
return Float::INFINITY unless validator.is_a?(ActiveModel::Validations::LengthValidator)
|
|
34
|
+
|
|
35
|
+
options = validator.options
|
|
36
|
+
options[:maximum] || options[:in]&.max || Float::INFINITY
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def validate_column_lengths
|
|
42
|
+
self.class.columns.each do |column|
|
|
43
|
+
name = column.name
|
|
44
|
+
next unless column.limit && will_save_change_to_attribute?(name)
|
|
45
|
+
|
|
46
|
+
check_column_length(name, column)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def check_column_length(name, column)
|
|
51
|
+
measure = MEASURES[column.type]
|
|
52
|
+
value = read_attribute(name)
|
|
53
|
+
return unless measure && value.is_a?(String) && value.public_send(measure) > column.limit
|
|
54
|
+
|
|
55
|
+
limit = column.limit
|
|
56
|
+
return if stricter_length_rule?(name, limit)
|
|
57
|
+
|
|
58
|
+
errors.add(name, :too_long, count: limit,
|
|
59
|
+
message: ->(*) { I18n.t('permission_forge.validations.too_long', count: limit) })
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# The attribute's own length rule already refuses everything the column
|
|
63
|
+
# cannot hold (its maximum is the column's or less): its message alone.
|
|
64
|
+
def stricter_length_rule?(name, limit)
|
|
65
|
+
self.class.validators_on(name).any? { |validator| ValidatesColumnLengths.maximum_of(validator) <= limit }
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Concerns
|
|
5
|
+
# Declarative validation for boolean permission field dependencies.
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# class Resource < ActiveRecord::Base
|
|
9
|
+
# include PermissionForge::Concerns::ValidatesDependency
|
|
10
|
+
#
|
|
11
|
+
# validates_dependency :edit_access, requires: :read_access
|
|
12
|
+
# validates_dependency :delete_access, requires: :read_access
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# resource = Resource.new(edit_access: true, read_access: false)
|
|
16
|
+
# resource.valid? # => false
|
|
17
|
+
# resource.errors[:edit_access] # => ["requires read_access to be enabled"]
|
|
18
|
+
#
|
|
19
|
+
# @api private
|
|
20
|
+
module ValidatesDependency
|
|
21
|
+
extend ActiveSupport::Concern
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
# Checks if this record belongs to a muting permission set.
|
|
26
|
+
# Works for both Resource (direct) and ResourceField (via resource).
|
|
27
|
+
def belongs_to_muting_permission_set?
|
|
28
|
+
ps = if respond_to?(:permission_set)
|
|
29
|
+
permission_set
|
|
30
|
+
elsif respond_to?(:resource) && resource.respond_to?(:permission_set)
|
|
31
|
+
resource.permission_set
|
|
32
|
+
end
|
|
33
|
+
ps&.is_muting == true
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class_methods do
|
|
37
|
+
# @param field [Symbol] the dependent boolean field
|
|
38
|
+
# @param requires [Symbol] the prerequisite boolean field
|
|
39
|
+
def validates_dependency(field, requires:)
|
|
40
|
+
method_name = :"validate_#{field}_requires_#{requires}"
|
|
41
|
+
|
|
42
|
+
validate method_name
|
|
43
|
+
|
|
44
|
+
define_method(method_name) do
|
|
45
|
+
return if belongs_to_muting_permission_set?
|
|
46
|
+
|
|
47
|
+
errors.add(field, "requires #{requires} to be enabled") if send(field) && !send(requires)
|
|
48
|
+
end
|
|
49
|
+
private method_name
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest/sha2'
|
|
4
|
+
require 'securerandom'
|
|
5
|
+
|
|
6
|
+
module PermissionForge
|
|
7
|
+
# The permission sets with activation conditions (session flag, IP
|
|
8
|
+
# allowlist, login hours) among those a user holds, and the variant of the
|
|
9
|
+
# permission hash a request context selects among them.
|
|
10
|
+
#
|
|
11
|
+
# A condition is checked against the request that READS the hash, never
|
|
12
|
+
# against the one that happened to build it (an MFA-verified session, an
|
|
13
|
+
# office IP, a job without a context). So the shared cache keeps, per user:
|
|
14
|
+
#
|
|
15
|
+
# - one base entry under permission_cache_key: the hash of the
|
|
16
|
+
# unconditional sets, in the public format, plus — only when the user
|
|
17
|
+
# holds conditional sets — this object under ENTRY_KEY (the conditions,
|
|
18
|
+
# and a token minted at that build);
|
|
19
|
+
# - one variant per combination of conditional sets that a context
|
|
20
|
+
# selects, in a slot of the base key: base key + KEY_INFIX + the
|
|
21
|
+
# bitmask of the selected sets among the user's conditional sets,
|
|
22
|
+
# sorted by id (#slot, 1..2^MAX_CACHED_SETS - 1). It holds the hash of
|
|
23
|
+
# the unconditional sets and those, in the public format, plus under
|
|
24
|
+
# VARIANT_KEY the fingerprint — a digest of the base's token and the
|
|
25
|
+
# selected ids — of the base entry and selection it was built for.
|
|
26
|
+
#
|
|
27
|
+
# A read serves a variant only when its fingerprint is the one the current
|
|
28
|
+
# base entry names (.variant_hash): a variant is reachable only through
|
|
29
|
+
# the base entry that minted its token. Anything else in the slot — the
|
|
30
|
+
# variant of a base entry that is gone — is a miss, rebuilt and written
|
|
31
|
+
# over. Every variant is stored through Cache::Rebuild under the user's
|
|
32
|
+
# invalidation stamp, so one built across an invalidation is discarded
|
|
33
|
+
# like a base.
|
|
34
|
+
#
|
|
35
|
+
# So an invalidation (PermissionCacheAccessor#invalidate_permissions,
|
|
36
|
+
# Cache::UserKeys) removes the base entries only: the variants it leaves
|
|
37
|
+
# can no longer be reached — every base build mints a new token — and go
|
|
38
|
+
# with their TTL (clamped to config.conditional_set_cache_ttl like the
|
|
39
|
+
# base's), or are written over. They still hold the profile, so where
|
|
40
|
+
# personal data must go now — Services::DataSubjectErasure, the
|
|
41
|
+
# enumerated cache:clear — Cache::VariantRemoval removes them: the slots
|
|
42
|
+
# of a base key (.variant_keys: a fixed set, whatever the token). Erasure
|
|
43
|
+
# removes every slot of each of the person's base keys without reading
|
|
44
|
+
# anything; the clear, over every user, only those of a base key that has
|
|
45
|
+
# an index entry (.index_key). The index entry says no more than that
|
|
46
|
+
# variants may be stored under the base key (INDEX), so concurrent writers
|
|
47
|
+
# write the same value; it is written after the variant's rebuild read
|
|
48
|
+
# the user's stamp and before the variant is written (a variant whose
|
|
49
|
+
# index entry could not be written is not stored), and outlives every
|
|
50
|
+
# variant written after it (INDEX_TTL_MARGIN). A user who never had a
|
|
51
|
+
# variant stored has none: the clear reads it and removes nothing.
|
|
52
|
+
#
|
|
53
|
+
# A user who holds more than MAX_CACHED_SETS conditional sets has the
|
|
54
|
+
# variants built for the request that asks and kept on its instance only,
|
|
55
|
+
# never stored or looked for — one build per request for such a user.
|
|
56
|
+
#
|
|
57
|
+
# Which conditional sets a context selects (#passing):
|
|
58
|
+
# - a context with an IP or session flags: those whose conditions pass
|
|
59
|
+
# (Services::PermissionSetConditionEvaluator), granting and muting alike;
|
|
60
|
+
# - no context at all — the resolver returned nothing (no resolver, as
|
|
61
|
+
# shipped): every conditional set, as before;
|
|
62
|
+
# - under config.conditional_sets_strict_mode, no request context — the
|
|
63
|
+
# resolver returned nothing, or neither an IP nor session flags (a job,
|
|
64
|
+
# the console; the time alone is not a request context): only the
|
|
65
|
+
# conditional MUTING sets, since a missing context must never grant and
|
|
66
|
+
# must never lift a lockdown either. With a request context, a muting
|
|
67
|
+
# set whose condition cannot be judged in it (an IP allowlist and no
|
|
68
|
+
# IP; a session flag and no :session_flags at all) applies too, for the
|
|
69
|
+
# same reason; granting sets are evaluated as they are. Without strict
|
|
70
|
+
# mode such a context is evaluated per set, as before: a job gets no
|
|
71
|
+
# MFA- or IP-gated grant, an IP-gated set fails without an IP, and the
|
|
72
|
+
# hours are checked.
|
|
73
|
+
# The time the conditions are checked at is fixed once per selection
|
|
74
|
+
# (.clocked), so the signature names the hour the evaluation saw.
|
|
75
|
+
#
|
|
76
|
+
# Everything here is plain data (Strings, Integers, Arrays, Data): it is
|
|
77
|
+
# written to the store as a Hash (#dump) and kept on a user instance in
|
|
78
|
+
# the memo (PermissionsMemo), which marshals with the user.
|
|
79
|
+
#
|
|
80
|
+
# @api private
|
|
81
|
+
class ConditionalSets < Data.define(:sets, :token)
|
|
82
|
+
# Where the base entry carries it.
|
|
83
|
+
ENTRY_KEY = :conditional_sets
|
|
84
|
+
# Where a stored variant carries the fingerprint of the base entry and
|
|
85
|
+
# selection it was built for: taken off on every read, never part of
|
|
86
|
+
# the hash.
|
|
87
|
+
VARIANT_KEY = :variant_of
|
|
88
|
+
# Between the base key and the slot in a variant's key.
|
|
89
|
+
KEY_INFIX = '_c_'
|
|
90
|
+
# After KEY_INFIX in the key of the index entry (a slot is a number).
|
|
91
|
+
INDEX_NAME = 'index'
|
|
92
|
+
# What the index entry holds: variants may be stored under the base key.
|
|
93
|
+
INDEX = true
|
|
94
|
+
# Seconds the index entry lives beyond config.cache_ttl, the longest a
|
|
95
|
+
# variant written after it lives.
|
|
96
|
+
INDEX_TTL_MARGIN = 60
|
|
97
|
+
# The most conditional sets a user may hold for the variants to be cached:
|
|
98
|
+
# erasure removes one slot per combination of them.
|
|
99
|
+
MAX_CACHED_SETS = 6
|
|
100
|
+
# Every slot a base key has.
|
|
101
|
+
SLOTS = (1..((1 << MAX_CACHED_SETS) - 1))
|
|
102
|
+
|
|
103
|
+
# One conditional set: what the evaluator reads of a PermissionSet.
|
|
104
|
+
#
|
|
105
|
+
# @api private
|
|
106
|
+
Member = Data.define(:id, :muting, :requires_session_flag, :allowed_ips, :allowed_hours_start, :allowed_hours_end) do
|
|
107
|
+
def self.of(permission_set)
|
|
108
|
+
new(id: permission_set.id, muting: permission_set.is_muting == true,
|
|
109
|
+
requires_session_flag: permission_set.requires_session_flag, allowed_ips: permission_set.allowed_ips,
|
|
110
|
+
allowed_hours_start: permission_set.allowed_hours_start, allowed_hours_end: permission_set.allowed_hours_end)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# A stored member — also one written by a version with fewer conditions.
|
|
114
|
+
#
|
|
115
|
+
# @raise [ArgumentError] for anything but a Hash
|
|
116
|
+
def self.load(data)
|
|
117
|
+
raise ArgumentError, "a conditional set is stored as a Hash, not #{data.class}" unless data.is_a?(Hash)
|
|
118
|
+
|
|
119
|
+
new(**members.to_h { |name| [name, data[name]] })
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def muting? = muting
|
|
123
|
+
|
|
124
|
+
# Whether the set counts in `context`: its conditions pass — or, in
|
|
125
|
+
# strict mode, it is a muting set whose condition cannot be judged
|
|
126
|
+
# there (no IP for an IP allowlist, no flags at all for a session flag).
|
|
127
|
+
def applies?(context, strict)
|
|
128
|
+
evaluator = Services::PermissionSetConditionEvaluator
|
|
129
|
+
evaluator.call(permission_set: self, context: context) ||
|
|
130
|
+
(strict && muting && evaluator.unjudgeable?(permission_set: self, context: context))
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# The conditional sets a context selected: their ids, the fingerprint
|
|
135
|
+
# of the base entry (token) and those ids, and whether the variant is
|
|
136
|
+
# kept in the shared cache (MAX_CACHED_SETS).
|
|
137
|
+
#
|
|
138
|
+
# @api private
|
|
139
|
+
Variant = Data.define(:ids, :fingerprint, :cached)
|
|
140
|
+
|
|
141
|
+
# What a memo keeps about the hash it holds: the user's conditional sets,
|
|
142
|
+
# the variant the hash was built for (nil: the base), and the signature
|
|
143
|
+
# of the context that selected it.
|
|
144
|
+
#
|
|
145
|
+
# @api private
|
|
146
|
+
Served = Data.define(:conditional, :variant, :signature) do
|
|
147
|
+
# Whether `context` is, as far as the conditions go, the one that
|
|
148
|
+
# selected the hash — cheaper than selecting again.
|
|
149
|
+
def selected_by?(context)
|
|
150
|
+
signature == ConditionalSets.signature(context)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# This selection, when `context` is the one that made it; a copy that
|
|
154
|
+
# remembers `context`, when it selects the same variant (so the next
|
|
155
|
+
# check is cheap again); nil when it selects another variant.
|
|
156
|
+
#
|
|
157
|
+
# @return [Served, nil]
|
|
158
|
+
def reselect(context)
|
|
159
|
+
return self if selected_by?(context)
|
|
160
|
+
|
|
161
|
+
again = conditional.serve(context)
|
|
162
|
+
again if again.variant == variant
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# The store key of the hash: the base key, or the variant's slot
|
|
166
|
+
# (one of ConditionalSets.variant_keys).
|
|
167
|
+
def store_key(base_key)
|
|
168
|
+
variant ? "#{base_key}#{KEY_INFIX}#{conditional.slot(variant.ids)}" : base_key
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
class << self
|
|
173
|
+
# @param permission_sets [Enumerable<PermissionSet>] the sets a user holds
|
|
174
|
+
# @return [ConditionalSets, nil] nil when none is conditional
|
|
175
|
+
def of(permission_sets)
|
|
176
|
+
sets = permission_sets.select(&:conditional?).map { |set| Member.of(set) }
|
|
177
|
+
new(sets: sets) if sets.any?
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# @param data [Hash] what #dump wrote
|
|
181
|
+
# @return [ConditionalSets]
|
|
182
|
+
# @raise [ArgumentError] for data without a token — another version of
|
|
183
|
+
# the gem, or a foreign writer, left it: without the token every base
|
|
184
|
+
# would name the same variants
|
|
185
|
+
def load(data)
|
|
186
|
+
token = data[:token] if data.is_a?(Hash)
|
|
187
|
+
raise ArgumentError, 'conditional sets are stored with a token' unless token.is_a?(String) && !token.empty?
|
|
188
|
+
|
|
189
|
+
new(sets: Array(data[:sets]).map { |member| Member.load(member) }, token: token)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Separates a stored base entry into the public hash and the conditional
|
|
193
|
+
# sets it carries. An entry without them is returned as it is; one whose
|
|
194
|
+
# conditional sets cannot be read is no entry at all.
|
|
195
|
+
#
|
|
196
|
+
# @return [Array(Hash, ConditionalSets|nil), nil]
|
|
197
|
+
def split(entry)
|
|
198
|
+
data = entry[ENTRY_KEY]
|
|
199
|
+
return [entry, nil] unless data
|
|
200
|
+
|
|
201
|
+
[entry.except(ENTRY_KEY), load(data)]
|
|
202
|
+
rescue ArgumentError
|
|
203
|
+
nil
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# The sets a hash is built from: the unconditional ones, and those of
|
|
207
|
+
# the variant when there is one.
|
|
208
|
+
#
|
|
209
|
+
# @param permission_sets [Enumerable<PermissionSet>]
|
|
210
|
+
# @param variant [Variant, nil]
|
|
211
|
+
# @return [Array<PermissionSet>]
|
|
212
|
+
def build_sets(permission_sets, variant)
|
|
213
|
+
return permission_sets.reject(&:conditional?) unless variant
|
|
214
|
+
|
|
215
|
+
ids = variant.ids
|
|
216
|
+
permission_sets.select { |set| set.unconditional? || ids.include?(set.id) }
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# @return [String] the store key of the index entry of the base entry
|
|
220
|
+
# at `base_key` (#index)
|
|
221
|
+
def index_key(base_key)
|
|
222
|
+
"#{base_key}#{KEY_INFIX}#{INDEX_NAME}"
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Every key a variant of the base entry at `base_key` can be stored
|
|
226
|
+
# under, whatever its token: one per slot (SLOTS).
|
|
227
|
+
#
|
|
228
|
+
# @param base_key [String]
|
|
229
|
+
# @return [Array<String>]
|
|
230
|
+
def variant_keys(base_key)
|
|
231
|
+
SLOTS.map { |slot| "#{base_key}#{KEY_INFIX}#{slot}" }
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# The entry a variant is stored as: the hash, and the fingerprint of
|
|
235
|
+
# the base entry and selection it was built for (VARIANT_KEY).
|
|
236
|
+
#
|
|
237
|
+
# @param hash [Hash] in the public format
|
|
238
|
+
# @param variant [Variant]
|
|
239
|
+
# @return [Hash]
|
|
240
|
+
def variant_entry(hash, variant)
|
|
241
|
+
hash.merge(VARIANT_KEY => variant.fingerprint)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# The hash a stored variant holds — when it was built for `variant`:
|
|
245
|
+
# this base entry (token) and this selection. nil for anything else in
|
|
246
|
+
# the slot: the variant of a base entry that is gone, another
|
|
247
|
+
# selection, an entry without a fingerprint.
|
|
248
|
+
#
|
|
249
|
+
# @param entry [Hash, nil] as read from the store
|
|
250
|
+
# @param variant [Variant]
|
|
251
|
+
# @return [Hash, nil] the hash in the public format
|
|
252
|
+
def variant_hash(entry, variant)
|
|
253
|
+
entry.except(VARIANT_KEY) if entry.is_a?(Hash) && entry[VARIANT_KEY] == variant.fingerprint
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# The digest of a token and sorted ids that names a variant.
|
|
257
|
+
#
|
|
258
|
+
# @return [String]
|
|
259
|
+
def fingerprint(token, ids)
|
|
260
|
+
Digest::SHA256.hexdigest("#{token}:#{ids.join(',')}")[0, 32]
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Whether `context` says nothing a condition could be checked against:
|
|
264
|
+
# the resolver returned nothing, or neither an IP nor session flags.
|
|
265
|
+
# The time alone is not a request context.
|
|
266
|
+
#
|
|
267
|
+
# @param context [Hash, nil] what config.request_context_resolver returned
|
|
268
|
+
# @return [Boolean]
|
|
269
|
+
def no_context?(context)
|
|
270
|
+
context.blank? || (context[:ip].blank? && Array(context[:session_flags]).empty?)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# The context with the time its conditions are checked at, fixed once:
|
|
274
|
+
# the hour the evaluation sees is the hour the signature names. A
|
|
275
|
+
# context that says nothing is left as it is (nothing is evaluated).
|
|
276
|
+
#
|
|
277
|
+
# @param context [Hash, #to_h, nil]
|
|
278
|
+
# @return [Hash]
|
|
279
|
+
def clocked(context)
|
|
280
|
+
context = context.to_h unless context.is_a?(Hash)
|
|
281
|
+
return context if context.blank? || context[:time]
|
|
282
|
+
|
|
283
|
+
context.merge(time: Time.current)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# What of a context the conditions look at: the hour, not the second;
|
|
287
|
+
# a copy of the flags (a resolver may hand out the array, or a flag, it
|
|
288
|
+
# mutates); and for a context that selects without evaluating — none
|
|
289
|
+
# at all, or none in strict mode — only that.
|
|
290
|
+
#
|
|
291
|
+
# @param context [Hash, nil] what config.request_context_resolver returned
|
|
292
|
+
# @return [Array, Symbol]
|
|
293
|
+
def signature(context)
|
|
294
|
+
return :strict if strict? && no_context?(context)
|
|
295
|
+
return :blank if context.blank?
|
|
296
|
+
|
|
297
|
+
flags = context[:session_flags]
|
|
298
|
+
[context[:ip], flags.nil? ? nil : Array(flags).map { |flag| -flag.to_s }, (context[:time] || Time.current).hour]
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def strict?
|
|
302
|
+
PermissionForge.configuration.conditional_sets_strict_mode == true
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# @param sets [Array<Member>]
|
|
307
|
+
# @param token [String] minted for the base entry that carries this
|
|
308
|
+
def initialize(sets:, token: SecureRandom.hex(8))
|
|
309
|
+
super(sets: sets.freeze, token: token)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# @return [Hash] plain data for the store (see .load)
|
|
313
|
+
def dump
|
|
314
|
+
{ token: token, sets: sets.map(&:to_h) }
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Whether the variants are kept in the shared cache (MAX_CACHED_SETS).
|
|
318
|
+
#
|
|
319
|
+
# @return [Boolean]
|
|
320
|
+
def cacheable?
|
|
321
|
+
sets.size <= MAX_CACHED_SETS
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# @param context [Hash, nil] what config.request_context_resolver returned
|
|
325
|
+
# @return [Served]
|
|
326
|
+
def serve(context)
|
|
327
|
+
context = self.class.clocked(context)
|
|
328
|
+
Served.new(conditional: self, variant: variant(context), signature: self.class.signature(context))
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# @return [Variant, nil] nil when the context selects no conditional set
|
|
332
|
+
def variant(context)
|
|
333
|
+
ids = passing(context).map(&:id).sort
|
|
334
|
+
return if ids.empty?
|
|
335
|
+
|
|
336
|
+
Variant.new(ids: ids, fingerprint: self.class.fingerprint(token, ids), cached: cacheable?)
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# The slot of the variant of `ids`: the bitmask of those sets among the
|
|
340
|
+
# user's conditional sets, sorted by id — 1..2^n - 1 for n sets, so
|
|
341
|
+
# within SLOTS for a user whose variants are cached (#cacheable?).
|
|
342
|
+
#
|
|
343
|
+
# @param ids [Array<Integer>] some of the sets' ids (Variant#ids)
|
|
344
|
+
# @return [Integer]
|
|
345
|
+
def slot(ids)
|
|
346
|
+
sorted = sets.map(&:id).sort
|
|
347
|
+
ids.sum { |id| 1 << sorted.index(id) }
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
private
|
|
351
|
+
|
|
352
|
+
def passing(context)
|
|
353
|
+
klass = self.class
|
|
354
|
+
strict = klass.strict?
|
|
355
|
+
return sets.select(&:muting?) if strict && klass.no_context?(context)
|
|
356
|
+
return sets if context.blank?
|
|
357
|
+
|
|
358
|
+
sets.select { |member| member.applies?(context, strict) }
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
end
|