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,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
# "What can this person do right now, and why?"
|
|
6
|
+
#
|
|
7
|
+
# Mirrors PermissionHashBuilder + FieldPermissionMerger exactly (same set
|
|
8
|
+
# selection order, same ||= / AND / muting semantics, same variants and
|
|
9
|
+
# extra fields) but keeps provenance: which Permission Set granted or muted
|
|
10
|
+
# every flag, field and action. Parity with the engine is covered by tests.
|
|
11
|
+
#
|
|
12
|
+
# Differences from a live request, by design:
|
|
13
|
+
# - Activation conditions are not evaluated (the admin's IP/time says
|
|
14
|
+
# nothing about the person's). Conditional sets count, and are reported in
|
|
15
|
+
# #conditional_sets so the UI can say "only while conditions are true".
|
|
16
|
+
class EffectiveAccess
|
|
17
|
+
CRUD = %i[read_access create_access edit_access delete_access].freeze
|
|
18
|
+
RESOURCE_FLAGS = (CRUD + %i[view_all modify_all]).freeze
|
|
19
|
+
|
|
20
|
+
# A permission outcome with the sets that caused it.
|
|
21
|
+
Outcome = Struct.new(:state, :granted_by, :blocked_by, keyword_init: true) do
|
|
22
|
+
def granted? = state == :granted
|
|
23
|
+
def blocked? = state == :blocked
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# `implicit` — no set configures the field for this outcome: it is a
|
|
27
|
+
# declared virtual field on the implicit read + edit grant
|
|
28
|
+
# (extra_fields_default_access = :grant).
|
|
29
|
+
FieldOutcome = Struct.new(:name, :level, :mask_strategy, :granted_by, :blocked_by, :blocked_level,
|
|
30
|
+
:capped, :unconfigured, :virtual, :variant_of, :implicit, keyword_init: true) do
|
|
31
|
+
def visible? = level != 'hidden'
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
RuleSource = Struct.new(:rule, :permission_set, keyword_init: true)
|
|
35
|
+
|
|
36
|
+
ResourceOutcome = Struct.new(:name, :flags, :records, :rules, :fields, :actions, keyword_init: true) do
|
|
37
|
+
def op(flag) = flags.fetch(flag)
|
|
38
|
+
def visible_field_count = fields.count(&:visible?)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# One resource name with its [permission_set, resource] pairs from the
|
|
42
|
+
# granting and muting sets, and the resulting resource flag outcomes.
|
|
43
|
+
ResourceSources = Struct.new(:name, :grants, :mutes, :flags, keyword_init: true)
|
|
44
|
+
|
|
45
|
+
attr_reader :live_sets, :conditional_sets
|
|
46
|
+
|
|
47
|
+
def self.call(...) = new(...).call
|
|
48
|
+
|
|
49
|
+
def initialize(user:, configuration: PermissionForge.configuration, catalog: ModelCatalog.new)
|
|
50
|
+
@user = user
|
|
51
|
+
@configuration = configuration
|
|
52
|
+
@catalog = catalog
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @return [Array<ResourceOutcome>]
|
|
56
|
+
def call
|
|
57
|
+
@live_sets = select_live_sets
|
|
58
|
+
@conditional_sets = @live_sets.select(&:conditional?)
|
|
59
|
+
granting, muting = @live_sets.partition(&:granting?)
|
|
60
|
+
|
|
61
|
+
names = granting.flat_map { |ps| active_resources(ps).map(&:name) }.uniq
|
|
62
|
+
names.map { |name| build_resource(sources_for(name, granting, muting)) }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
# Same selection as PermissionCacheAccessor#active_non_expired_permission_sets
|
|
68
|
+
# minus the request-context filter.
|
|
69
|
+
def select_live_sets
|
|
70
|
+
ids = PermissionForge::PermissionSetUser.where(user_id: @user.id).active_assignments.select(:permission_set_id)
|
|
71
|
+
sets = PermissionForge::PermissionSet.active.where(id: ids).order(:created_at, :id)
|
|
72
|
+
.includes(resources: %i[resource_fields resource_actions permission_set_rules]).to_a
|
|
73
|
+
# Same row order as the hash builder (a variant tie goes to the older row).
|
|
74
|
+
sets.each { |permission_set| PermissionForge::Services::PermissionHashBuilder.sort_rows(permission_set) }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def active_resources(permission_set)
|
|
78
|
+
permission_set.resources.select(&:is_active)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# [permission_set, resource] for every active row of the sets named by
|
|
82
|
+
# one of `names`, in set order then row order.
|
|
83
|
+
def pairs_for(permission_sets, names)
|
|
84
|
+
permission_sets.flat_map do |ps|
|
|
85
|
+
active_resources(ps).select { |r| names.include?(r.name) }.map { |resource| [ps, resource] }
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# A granting set's row for the resource; a muting set's rows for the
|
|
90
|
+
# resource and for the STI bases of the model it names — the engine's
|
|
91
|
+
# cascade (PermissionHashBuilder#apply_muting_permission_set): a muted
|
|
92
|
+
# base reaches the subclass entry, a muted subclass only itself.
|
|
93
|
+
def sources_for(name, granting, muting)
|
|
94
|
+
grants = pairs_for(granting, [name])
|
|
95
|
+
mutes = pairs_for(muting, PermissionForge::ResourceChain.names_for(name).map(&:to_s))
|
|
96
|
+
flags = RESOURCE_FLAGS.index_with { |flag| outcome_for(grants, mutes) { |r| r.public_send(flag) } }
|
|
97
|
+
ResourceSources.new(name: name, grants: grants, mutes: mutes, flags: flags)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def build_resource(sources)
|
|
101
|
+
grants = sources.grants
|
|
102
|
+
ResourceOutcome.new(
|
|
103
|
+
name: sources.name, flags: sources.flags,
|
|
104
|
+
records: records_state(sources.flags),
|
|
105
|
+
rules: rules_for(grants),
|
|
106
|
+
fields: EffectiveFields.new(sources, configuration: @configuration, catalog: @catalog).call,
|
|
107
|
+
actions: action_outcomes(grants, sources.mutes)
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Granted by every granting set whose resource matches; muting sets only
|
|
112
|
+
# block what some set granted.
|
|
113
|
+
def outcome_for(grants, mutes, &)
|
|
114
|
+
granted_by = sets_matching(grants, &)
|
|
115
|
+
blocked_by = granted_by.any? ? sets_matching(mutes, &) : []
|
|
116
|
+
Outcome.new(state: outcome_state(granted_by, blocked_by), granted_by: granted_by, blocked_by: blocked_by)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Each set once: a muting set may reach an entry through two rows.
|
|
120
|
+
def sets_matching(pairs)
|
|
121
|
+
pairs.select { |_, resource| yield resource }.map(&:first).uniq
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def outcome_state(granted_by, blocked_by)
|
|
125
|
+
return :none if granted_by.empty?
|
|
126
|
+
|
|
127
|
+
blocked_by.any? ? :blocked : :granted
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def records_state(flags)
|
|
131
|
+
return :none unless flags[:read_access].granted?
|
|
132
|
+
|
|
133
|
+
flags[:view_all].granted? ? :all : :rules
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def rules_for(grants)
|
|
137
|
+
grants.flat_map do |ps, resource|
|
|
138
|
+
resource.permission_set_rules.select(&:is_active).map { |rule| RuleSource.new(rule: rule, permission_set: ps) }
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# --- Actions ----------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
def action_outcomes(grants, mutes)
|
|
145
|
+
names = grants.flat_map { |_, r| active_actions(r).map(&:name) }.uniq
|
|
146
|
+
names.map { |action| [action, outcome_for(grants, mutes) { |r| permitted?(r, action) }] }
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def active_actions(resource)
|
|
150
|
+
resource.resource_actions.select(&:is_active)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def permitted?(resource, action)
|
|
154
|
+
active_actions(resource).select(&:is_permitted).map(&:name).include?(action)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
# Field half of EffectiveAccess for one resource: mirrors what
|
|
6
|
+
# PermissionHashBuilder does with fields (FieldPermissionMerger's ||=
|
|
7
|
+
# read/edit, AND-merged mask and variants, then extra fields, then muting)
|
|
8
|
+
# while keeping which sets granted or muted each field.
|
|
9
|
+
#
|
|
10
|
+
# Extra fields follow the engine: after every granting set is merged, each
|
|
11
|
+
# declared name that nothing configured gets the implicit entry
|
|
12
|
+
# (ExtraFieldDefaults). A row in any set wins, whatever the set order.
|
|
13
|
+
class EffectiveFields
|
|
14
|
+
FieldOutcome = EffectiveAccess::FieldOutcome
|
|
15
|
+
|
|
16
|
+
# What FiltersResponseAttributes does with a column no set mentions
|
|
17
|
+
# (any other strategy hides it).
|
|
18
|
+
UNCONFIGURED_LEVELS = { 'pass_through' => 'read', 'warn' => 'read', 'mask_all' => 'masked' }.freeze
|
|
19
|
+
|
|
20
|
+
# @param sources [EffectiveAccess::ResourceSources]
|
|
21
|
+
def initialize(sources, configuration:, catalog:)
|
|
22
|
+
@sources = sources
|
|
23
|
+
@configuration = configuration
|
|
24
|
+
@catalog = catalog
|
|
25
|
+
# entry: { read_access:, edit_access:, mask_access:, mask_strategy:, sources: [[set, level]], variant_of: }
|
|
26
|
+
@entries = {}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @return [Array<EffectiveAccess::FieldOutcome>]
|
|
30
|
+
def call
|
|
31
|
+
merge_granting_fields
|
|
32
|
+
apply_muting_fields
|
|
33
|
+
|
|
34
|
+
model = @catalog.find(@sources.name)
|
|
35
|
+
(@entries.keys + Array(model&.managed_field_names)).uniq.map do |field|
|
|
36
|
+
field_outcome(field, model.present? && model.virtual_field?(field))
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def active_fields(resource)
|
|
43
|
+
resource.resource_fields.select(&:is_active)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def variants_of(name)
|
|
47
|
+
@configuration.field_variants_for(name)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def merge_granting_fields
|
|
51
|
+
@sources.grants.each do |ps, resource|
|
|
52
|
+
active_fields(resource).each { |field| merge_granting_field(ps, field) }
|
|
53
|
+
end
|
|
54
|
+
implicit_names.each { |name| @entries[name] ||= implicit_entry }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def merge_granting_field(permission_set, field)
|
|
58
|
+
name = field.name
|
|
59
|
+
variants_of(name).each { |variant| merge_into(entry_for(variant, name), field, permission_set) }
|
|
60
|
+
merge_into(entry_for(name, nil), field, permission_set)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def entry_for(key, variant_of)
|
|
64
|
+
@entries[key] ||= { sources: [], variant_of: variant_of }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# The engine's own merge (FieldPermissionMerger.merge_row), plus provenance.
|
|
68
|
+
def merge_into(entry, field, permission_set)
|
|
69
|
+
Services::FieldPermissionMerger.merge_row(entry, field)
|
|
70
|
+
entry[:sources] << [permission_set, FieldLevel.level_of(field, muting: false), field.mask_strategy]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Declared names that get the implicit read + edit entry; none under
|
|
74
|
+
# extra_fields_default_access = :unconfigured (they are then listed as
|
|
75
|
+
# unconfigured virtual fields instead). Read for any constant, like the
|
|
76
|
+
# engine does, not only for the models the catalog lists.
|
|
77
|
+
def implicit_names
|
|
78
|
+
PermissionForge::VirtualFields.new(@sources.name.to_s.safe_constantize, @configuration).implicit
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# The engine's entry for the resource flags as the granting sets merged
|
|
82
|
+
# them (what some granting set grants, blocked later or not).
|
|
83
|
+
def implicit_entry
|
|
84
|
+
granted = @sources.flags.transform_values { |outcome| !outcome.granted_by.empty? }
|
|
85
|
+
Services::ExtraFieldDefaults.implicit_entry(granted).merge(sources: [], extra: true)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def apply_muting_fields
|
|
89
|
+
@sources.mutes.each do |ps, resource|
|
|
90
|
+
active_fields(resource).each { |field| apply_muting_field(ps, field) }
|
|
91
|
+
hide_every_field(ps) if resource.read_access
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# A muting set that revokes the resource's read hides every field, as
|
|
96
|
+
# the engine does (PermissionHashBuilder#mute_fields_of_resource). One
|
|
97
|
+
# that revokes its edit caps every field to read: level_attrs does that
|
|
98
|
+
# from the resource's edit outcome.
|
|
99
|
+
def hide_every_field(permission_set)
|
|
100
|
+
@entries.each_value { |entry| hide_entry(entry, permission_set) }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def hide_entry(entry, permission_set)
|
|
104
|
+
entry[:read_access] = false
|
|
105
|
+
entry[:edit_access] = false
|
|
106
|
+
(entry[:mutes] ||= []) << [permission_set, 'hide']
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def apply_muting_field(permission_set, field)
|
|
110
|
+
name = field.name
|
|
111
|
+
([name] + variants_of(name)).each do |key|
|
|
112
|
+
entry = @entries[key]
|
|
113
|
+
mute_field(entry, field, permission_set) if entry
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# The engine's own muting of a row (PermissionHashBuilder
|
|
118
|
+
# #mute_single_field: a row that mutes read mutes edit too), plus
|
|
119
|
+
# provenance.
|
|
120
|
+
def mute_field(entry, field, permission_set)
|
|
121
|
+
level = FieldLevel.level_of(field, muting: true)
|
|
122
|
+
return if level == 'leave'
|
|
123
|
+
|
|
124
|
+
entry[:read_access] = false if field.read_access
|
|
125
|
+
entry[:edit_access] = false if field.read_access || field.edit_access
|
|
126
|
+
entry[:mask_access] = true if field.mask_access
|
|
127
|
+
(entry[:mutes] ||= []) << [permission_set, level]
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def field_outcome(name, virtual)
|
|
131
|
+
entry = @entries[name]
|
|
132
|
+
return unconfigured_outcome(name, virtual) if entry.nil?
|
|
133
|
+
|
|
134
|
+
implicit = entry[:extra].present?
|
|
135
|
+
FieldOutcome.new(
|
|
136
|
+
name: name, mask_strategy: entry[:mask_strategy], granted_by: granting_sources(entry),
|
|
137
|
+
unconfigured: false, virtual: virtual || implicit, implicit: implicit, variant_of: entry[:variant_of],
|
|
138
|
+
**level_attrs(entry), **muting_attrs(entry)
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Editable fields are capped to Read when the resource's Edit is not granted.
|
|
143
|
+
def level_attrs(entry)
|
|
144
|
+
level = FieldLevel.effective_level(entry)
|
|
145
|
+
capped = level == 'edit' && !@sources.flags[:edit_access].granted?
|
|
146
|
+
{ level: capped ? 'read' : level, capped: capped }
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def muting_attrs(entry)
|
|
150
|
+
mutes = entry[:mutes] || []
|
|
151
|
+
{ blocked_by: mutes.map(&:first).uniq, blocked_level: mutes.last&.last }
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Sets that make the field readable, most open first. A set that lists
|
|
155
|
+
# the field as Hidden grants nothing and, like the engine, has no say in
|
|
156
|
+
# the mask. An implicit entry exists only when no set configured the
|
|
157
|
+
# field, so no set is credited for it: the level comes from the
|
|
158
|
+
# virtual-field default.
|
|
159
|
+
def granting_sources(entry)
|
|
160
|
+
return [] if entry[:extra]
|
|
161
|
+
|
|
162
|
+
entry[:sources].select { |_, lvl, _| FieldLevel.rank(lvl) >= 1 }
|
|
163
|
+
.sort_by { |_, lvl, _| -FieldLevel.rank(lvl) }.map(&:first).uniq
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def unconfigured_outcome(name, virtual)
|
|
167
|
+
FieldOutcome.new(name: name, level: unconfigured_level, mask_strategy: 'full', granted_by: [],
|
|
168
|
+
blocked_by: [], capped: false, unconfigured: true, virtual: virtual, implicit: false)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def unconfigured_level
|
|
172
|
+
UNCONFIGURED_LEVELS.fetch(@configuration.unconfigured_field_strategy.to_s, 'hidden')
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
# Assignment end-date presets. Calendar-day maths (date + n days), never
|
|
6
|
+
# seconds, so DST changes don't shift labels. An assignment ending on a
|
|
7
|
+
# date stays valid through the end of that day.
|
|
8
|
+
module ExpiryPresets
|
|
9
|
+
DAYS = { '1d' => 1, '1w' => 7, '30d' => 30 }.freeze
|
|
10
|
+
KEYS = %w[1d 1w 30d eom custom never].freeze
|
|
11
|
+
CHANGE_KEYS = %w[1w 30d eom never].freeze
|
|
12
|
+
|
|
13
|
+
class InvalidDate < StandardError; end
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
# @return [Date, nil] nil for "never"/"custom" without a date
|
|
18
|
+
def date_for(key, today: Time.zone.today, custom: nil)
|
|
19
|
+
case key.to_s
|
|
20
|
+
when *DAYS.keys then today + DAYS.fetch(key.to_s)
|
|
21
|
+
when 'eom' then today.end_of_month
|
|
22
|
+
when 'custom' then parse_custom(custom, today)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# The expires_at timestamp for a preset (end of the chosen day), or nil.
|
|
27
|
+
def expires_at_for(key, today: Time.zone.today, custom: nil)
|
|
28
|
+
date_for(key, today: today, custom: custom)&.in_time_zone&.end_of_day
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def parse_custom(value, today)
|
|
32
|
+
raise InvalidDate, :missing if value.blank?
|
|
33
|
+
|
|
34
|
+
date = Date.iso8601(value.to_s)
|
|
35
|
+
raise InvalidDate, :not_future unless date > today
|
|
36
|
+
|
|
37
|
+
date
|
|
38
|
+
rescue Date::Error
|
|
39
|
+
raise InvalidDate, :missing
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Days until the assignment ends (0 = today, negative = ended), nil when
|
|
43
|
+
# it never ends.
|
|
44
|
+
def days_left(expires_at, today: Time.zone.today)
|
|
45
|
+
return nil if expires_at.nil?
|
|
46
|
+
|
|
47
|
+
(expires_at.to_date - today).to_i
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
module Exports
|
|
6
|
+
# A file the admin hands over (CSV or JSON): a metadata block — what was
|
|
7
|
+
# exported, when, by whom, with which filters, how many rows — then
|
|
8
|
+
# every row. Rows are produced lazily, one batch at a time, so a large
|
|
9
|
+
# export is written as it is read and nothing is cut off.
|
|
10
|
+
#
|
|
11
|
+
# CSV layout:
|
|
12
|
+
# export,<kind>
|
|
13
|
+
# exported_at,<ISO 8601>
|
|
14
|
+
# exported_by,<label> (and exported_by_type / exported_by_id)
|
|
15
|
+
# filters,<JSON>
|
|
16
|
+
# row_count,<n>
|
|
17
|
+
# <empty line>
|
|
18
|
+
# <column names>
|
|
19
|
+
# <rows>
|
|
20
|
+
#
|
|
21
|
+
# JSON layout: { "meta": {...}, "columns": [...], "data": [ {...}, ... ] }
|
|
22
|
+
class Document
|
|
23
|
+
attr_reader :columns, :meta
|
|
24
|
+
|
|
25
|
+
# @param columns [Array<String>] the row keys, in order
|
|
26
|
+
# @param meta [Hash{String => Object}] export, exported_at, exported_by, filters, row_count …
|
|
27
|
+
# @param rows [#call] callable taking a block; yields each row as a Hash{String => Object}
|
|
28
|
+
def initialize(columns:, meta:, rows:)
|
|
29
|
+
@columns = columns
|
|
30
|
+
@meta = meta
|
|
31
|
+
@rows = rows
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @return [Enumerator<String>] the CSV, chunk by chunk
|
|
35
|
+
def csv
|
|
36
|
+
Enumerator.new do |out|
|
|
37
|
+
out << csv_head
|
|
38
|
+
@rows.call { |row| out << csv_row(row) }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# The metadata block, an empty line, the column names.
|
|
43
|
+
def csv_head
|
|
44
|
+
Csv.document(meta.map { |key, value| [key, flat(value)] }) + Csv::LINE_END + Csv.line(columns)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @return [Enumerator<String>] the JSON, chunk by chunk
|
|
48
|
+
def json
|
|
49
|
+
Enumerator.new do |out|
|
|
50
|
+
out << %({"meta":#{ActiveSupport::JSON.encode(meta)},"columns":#{ActiveSupport::JSON.encode(columns)},"data":[)
|
|
51
|
+
first = true
|
|
52
|
+
@rows.call do |row|
|
|
53
|
+
out << (first ? '' : ',') << ActiveSupport::JSON.encode(row.slice(*columns))
|
|
54
|
+
first = false
|
|
55
|
+
end
|
|
56
|
+
out << "]}\n"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @param format [Symbol, String] :csv or :json
|
|
61
|
+
# @return [Enumerator<String>]
|
|
62
|
+
def chunks(format)
|
|
63
|
+
format.to_s == 'json' ? json : csv
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# The whole file as one String (tests, small exports).
|
|
67
|
+
def render(format)
|
|
68
|
+
chunks(format).to_a.join
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
# One row as a CSV line, in column order.
|
|
74
|
+
def csv_row(row)
|
|
75
|
+
Csv.line(columns.map { |column| flat(row[column]) })
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# A cell: Hashes and Arrays as JSON, times in ISO 8601, the rest as text.
|
|
79
|
+
def flat(value)
|
|
80
|
+
case value
|
|
81
|
+
when Hash, Array then ActiveSupport::JSON.encode(value)
|
|
82
|
+
when Time, DateTime, ActiveSupport::TimeWithZone then value.utc.iso8601(6)
|
|
83
|
+
else value
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
module Exports
|
|
6
|
+
# The audit log export: every event the filter matches (views of the
|
|
7
|
+
# log included, whatever the page hid), oldest first,
|
|
8
|
+
# with every column (the JSON API's, from AuditEventSerializer) and
|
|
9
|
+
# `data` as JSON. The rows are fixed when the export starts — the
|
|
10
|
+
# events up to the newest one then — so the row count in the metadata
|
|
11
|
+
# is the number of rows that follow, even while new events (this
|
|
12
|
+
# export's own audit.exported, for one) are being written.
|
|
13
|
+
class Events
|
|
14
|
+
BATCH_SIZE = 1000
|
|
15
|
+
KIND = 'audit_events'
|
|
16
|
+
|
|
17
|
+
def self.columns
|
|
18
|
+
PermissionForge::Api::Serializers::AuditEventSerializer.attribute_names.map(&:to_s)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @param relation [ActiveRecord::Relation] the filtered events, in any order
|
|
22
|
+
# @param filters [Hash] the filters, as written in the metadata
|
|
23
|
+
# @param exported_by [Audit::Actor]
|
|
24
|
+
# @param batch_size [Integer] events read per query
|
|
25
|
+
# @param time_zone [String] the zone date-only filters were read in
|
|
26
|
+
# @return [Document]
|
|
27
|
+
def self.call(relation, filters:, exported_by: PermissionForge::Audit::Actor.current, now: Time.current, # rubocop:disable Metrics/ParameterLists
|
|
28
|
+
batch_size: BATCH_SIZE, time_zone: AuditLogDays.zone_name)
|
|
29
|
+
events = snapshot(relation.unscope(:order))
|
|
30
|
+
# An export is evidence: views of the log are always in it, and it says so.
|
|
31
|
+
meta = Exports.meta(KIND, exported_by: exported_by, filters: filters.to_h.merge('include_views' => true),
|
|
32
|
+
row_count: events.count, now: now, time_zone: time_zone)
|
|
33
|
+
Document.new(columns: columns, meta: meta, rows: lambda { |&row|
|
|
34
|
+
events.find_each(batch_size: batch_size) { |event| row.call(serialize(event)) }
|
|
35
|
+
})
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.snapshot(events)
|
|
39
|
+
newest = events.maximum(:id)
|
|
40
|
+
newest ? events.where(id: ..newest) : events.none
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.serialize(event)
|
|
44
|
+
PermissionForge::Api::Serializers::AuditEventSerializer.new(event).as_json.transform_keys(&:to_s)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private_class_method :snapshot, :serialize
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
module Exports
|
|
6
|
+
# "Who holds what": every assignment as it stands — the person, the
|
|
7
|
+
# Permission Set, when and by whom it was granted, when it ends, when
|
|
8
|
+
# and by whom it was last reviewed, and the reason of its last change.
|
|
9
|
+
# Granted by is the actor of the assignment.granted event when the
|
|
10
|
+
# audit log has one, else the created_by column.
|
|
11
|
+
#
|
|
12
|
+
# The rows are read while the file is written (batches of BATCH_SIZE,
|
|
13
|
+
# the way the events export does), never all at once. `row_count` in the
|
|
14
|
+
# metadata is a COUNT taken when the export starts, on the assignments up
|
|
15
|
+
# to the newest id then. Assignments are changed and removed, not only
|
|
16
|
+
# added: one removed while the file is written is missing from it
|
|
17
|
+
# (`row_count` can then exceed the rows), and each row shows its
|
|
18
|
+
# assignment as it was when its batch was read. The person lookups run
|
|
19
|
+
# per batch, in the tenant and with the record rules suspended as in the
|
|
20
|
+
# action (AuditEventsController#pf_stream).
|
|
21
|
+
class Holdings
|
|
22
|
+
BATCH_SIZE = 500
|
|
23
|
+
KIND = 'holdings'
|
|
24
|
+
COLUMNS = %w[assignment_id assignment_uuid person_id person permission_set_id permission_set_uuid permission_set
|
|
25
|
+
muting set_active granted_at granted_by_type granted_by_id granted_by expires_at expired
|
|
26
|
+
reviewed_at reviewed_by_id reviewed_by reason].freeze
|
|
27
|
+
|
|
28
|
+
# @param exported_by [Audit::Actor]
|
|
29
|
+
# @param batch_size [Integer] assignments read per query
|
|
30
|
+
# @return [Document]
|
|
31
|
+
def self.call(exported_by: PermissionForge::Audit::Actor.current, now: Time.current, batch_size: BATCH_SIZE)
|
|
32
|
+
new(now, batch_size).document(exported_by)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def initialize(now, batch_size = BATCH_SIZE)
|
|
36
|
+
@now = now
|
|
37
|
+
@batch_size = batch_size
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def document(exported_by)
|
|
41
|
+
assignments = snapshot
|
|
42
|
+
meta = Exports.meta(KIND, exported_by: exported_by, filters: {}, row_count: assignments.count, now: @now)
|
|
43
|
+
Document.new(columns: COLUMNS, meta: meta, rows: ->(&row) { each_row(assignments, &row) })
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
# The assignments as they stand now: up to the newest id.
|
|
49
|
+
def snapshot
|
|
50
|
+
assignments = PermissionForge::PermissionSetUser.includes(:permission_set, :user)
|
|
51
|
+
newest = assignments.maximum(:id)
|
|
52
|
+
newest ? assignments.where(id: ..newest) : assignments.none
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Outside the query cache: it would keep every batch's results for the request.
|
|
56
|
+
def each_row(assignments, &)
|
|
57
|
+
ActiveRecord::Base.uncached { each_batch(assignments, &) }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def each_batch(assignments)
|
|
61
|
+
assignments.find_in_batches(batch_size: @batch_size) do |batch|
|
|
62
|
+
granted = LastChanges.new(batch, events: %w[assignment.granted])
|
|
63
|
+
people = people_by_id(batch)
|
|
64
|
+
batch.each { |assignment| yield row(assignment, granted[assignment], people) }
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def row(assignment, grant_event, people)
|
|
69
|
+
set = assignment.permission_set
|
|
70
|
+
person(assignment).merge(permission_set_columns(set)).merge(
|
|
71
|
+
'granted_at' => assignment.created_at, **granted_by(assignment, grant_event, people),
|
|
72
|
+
'expires_at' => assignment.expires_at, 'expired' => assignment.expired?,
|
|
73
|
+
**reviewed(assignment, people), 'reason' => assignment.try(:reason)
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def person(assignment)
|
|
78
|
+
{ 'assignment_id' => assignment.id, 'assignment_uuid' => assignment.try(:uuid), 'person_id' => assignment.user_id,
|
|
79
|
+
'person' => label(assignment.user) }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def permission_set_columns(set)
|
|
83
|
+
{ 'permission_set_id' => set&.id, 'permission_set_uuid' => set&.uuid, 'permission_set' => set&.name,
|
|
84
|
+
'muting' => set&.is_muting, 'set_active' => set&.is_active }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def granted_by(assignment, event, people)
|
|
88
|
+
if event
|
|
89
|
+
return { 'granted_by_type' => event.actor_type, 'granted_by_id' => event.actor_id,
|
|
90
|
+
'granted_by' => event.actor_label }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
id = assignment.try(:created_by_id)
|
|
94
|
+
{ 'granted_by_type' => (id ? 'user' : nil), 'granted_by_id' => id, 'granted_by' => label(people[id.to_s]) }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def reviewed(assignment, people)
|
|
98
|
+
id = assignment.try(:reviewed_by_id)
|
|
99
|
+
{ 'reviewed_at' => assignment.try(:reviewed_at), 'reviewed_by_id' => id, 'reviewed_by' => label(people[id.to_s]) }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# The creators and reviewers of a batch, by id (String).
|
|
103
|
+
def people_by_id(batch)
|
|
104
|
+
ids = batch.flat_map { |assignment| [assignment.try(:created_by_id), assignment.try(:reviewed_by_id)] }.compact.uniq
|
|
105
|
+
return {} if ids.empty?
|
|
106
|
+
|
|
107
|
+
PermissionForge.configuration.user_model.where(id: ids).index_by { |user| user.id.to_s }
|
|
108
|
+
rescue StandardError
|
|
109
|
+
{}
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def label(user)
|
|
113
|
+
PersonLabel.new(user).name if user
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
# Files the admin hands over: the audit log (Exports::Events) and who
|
|
6
|
+
# holds what (Exports::Holdings), written by Exports::Document.
|
|
7
|
+
module Exports
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
# The metadata block every export starts with.
|
|
11
|
+
#
|
|
12
|
+
# @param kind [String] what was exported
|
|
13
|
+
# @param exported_by [Audit::Actor]
|
|
14
|
+
# @param filters [Hash]
|
|
15
|
+
# @param row_count [Integer]
|
|
16
|
+
# @param time_zone [String] the zone date-only filters were read in
|
|
17
|
+
# @return [Hash{String => Object}]
|
|
18
|
+
def meta(kind, exported_by:, filters:, row_count:, now: Time.current, time_zone: AuditLogDays.zone_name) # rubocop:disable Metrics/ParameterLists
|
|
19
|
+
{
|
|
20
|
+
'export' => kind,
|
|
21
|
+
'exported_at' => now.utc.iso8601,
|
|
22
|
+
'time_zone' => time_zone,
|
|
23
|
+
'exported_by' => exported_by.label || exported_by.type,
|
|
24
|
+
'exported_by_type' => exported_by.type,
|
|
25
|
+
'exported_by_id' => exported_by.id,
|
|
26
|
+
'filters' => filters.to_h.transform_keys(&:to_s),
|
|
27
|
+
'row_count' => row_count
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# A file name: permission-forge-<kind>-<UTC timestamp>.<format>
|
|
32
|
+
def filename(kind, format, now: Time.current)
|
|
33
|
+
"permission-forge-#{kind.to_s.tr('_', '-')}-#{now.utc.strftime('%Y%m%dT%H%M%SZ')}.#{format}"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|