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,98 @@
|
|
|
1
|
+
class PermissionForgeAddAuditEvents < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
# Creates the insert-only audit log and the columns the access review and the
|
|
3
|
+
# "This is intended" acknowledgement record. Every step is guarded, so running
|
|
4
|
+
# it against an install that already has them changes nothing. Existing
|
|
5
|
+
# assignments are not backfilled: a blank reviewed_at means "never reviewed".
|
|
6
|
+
def up
|
|
7
|
+
create_audit_events unless table_exists?(:<%= table_prefix %>audit_events)
|
|
8
|
+
add_tenant_column
|
|
9
|
+
add_audit_indexes
|
|
10
|
+
|
|
11
|
+
add_column_once :<%= table_prefix %>permission_set_users, :reviewed_at, :datetime
|
|
12
|
+
add_column_once :<%= table_prefix %>permission_set_users, :reviewed_by_id, user_key_type
|
|
13
|
+
add_column_once :<%= table_prefix %>permission_set_users, :reason, :string
|
|
14
|
+
add_column_once :<%= table_prefix %>permission_set_group_users, :reason, :string
|
|
15
|
+
add_column_once :<%= table_prefix %>resource_fields, :sensitive_acknowledged_by_id, user_key_type
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# The audit log is evidence, and `up` may have changed nothing because the
|
|
19
|
+
# install already owned these objects, so a rollback must never delete it.
|
|
20
|
+
def down
|
|
21
|
+
raise ActiveRecord::IrreversibleMigration,
|
|
22
|
+
'The audit log is evidence: permission_forge_add_audit_events does not drop the audit_events table ' \
|
|
23
|
+
'or its columns on rollback. Remove them deliberately, by hand, if you really mean to.'
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
# No foreign keys: events outlive the records they describe.
|
|
29
|
+
def create_audit_events
|
|
30
|
+
create_table :<%= table_prefix %>audit_events do |t|
|
|
31
|
+
t.string :uuid, null: false
|
|
32
|
+
t.datetime :occurred_at, null: false, precision: 6
|
|
33
|
+
t.string :event, null: false
|
|
34
|
+
t.string :category, null: false
|
|
35
|
+
t.string :actor_type, null: false
|
|
36
|
+
t.string :actor_id
|
|
37
|
+
t.string :actor_label
|
|
38
|
+
t.string :source, null: false
|
|
39
|
+
t.string :record_type
|
|
40
|
+
t.string :record_id
|
|
41
|
+
t.string :record_uuid
|
|
42
|
+
t.string :record_label
|
|
43
|
+
t.bigint :permission_set_id
|
|
44
|
+
t.string :permission_set_uuid
|
|
45
|
+
t.string :subject_user_id
|
|
46
|
+
t.text :data
|
|
47
|
+
t.string :reason
|
|
48
|
+
t.string :request_id
|
|
49
|
+
t.string :ip
|
|
50
|
+
t.string :correlation_id
|
|
51
|
+
<% if tenant_column %> t.column :<%= tenant_column %>, tenant_key_type
|
|
52
|
+
<% end %> t.datetime :created_at, null: false
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# A table made by an earlier run without tenancy gets the column before its index.
|
|
57
|
+
def add_tenant_column
|
|
58
|
+
<% if tenant_column %> add_column_once :<%= table_prefix %>audit_events, :<%= tenant_column %>, tenant_key_type
|
|
59
|
+
<% end %> end
|
|
60
|
+
|
|
61
|
+
def add_audit_indexes
|
|
62
|
+
table = :<%= table_prefix %>audit_events
|
|
63
|
+
add_index_once table, :uuid, unique: true, name: 'idx_pf_audit_uuid'
|
|
64
|
+
add_index_once table, :occurred_at, name: 'idx_pf_audit_occurred'
|
|
65
|
+
add_index_once table, %i[event occurred_at], name: 'idx_pf_audit_event'
|
|
66
|
+
add_index_once table, %i[subject_user_id occurred_at], name: 'idx_pf_audit_subject'
|
|
67
|
+
add_index_once table, %i[permission_set_id occurred_at], name: 'idx_pf_audit_pset'
|
|
68
|
+
add_index_once table, %i[permission_set_uuid occurred_at], name: 'idx_pf_audit_pset_uuid'
|
|
69
|
+
add_index_once table, %i[record_type record_id occurred_at], name: 'idx_pf_audit_record'
|
|
70
|
+
add_index_once table, %i[actor_id occurred_at], name: 'idx_pf_audit_actor'
|
|
71
|
+
add_index_once table, :correlation_id, name: 'idx_pf_audit_correlation'
|
|
72
|
+
<% if tenant_column %> add_index_once table, :<%= tenant_column %>, name: 'idx_pf_audit_tenant'
|
|
73
|
+
<% end %> end
|
|
74
|
+
|
|
75
|
+
# User and tenant ids keep the type the install already uses (a UUID or
|
|
76
|
+
# string key must not be truncated by a bigint column); bigint when unknown.
|
|
77
|
+
def user_key_type
|
|
78
|
+
key_type_of(:<%= table_prefix %>permission_set_users, :user_id)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def tenant_key_type
|
|
82
|
+
<% if tenant_column %> key_type_of(:<%= table_prefix %>permission_sets, :<%= tenant_column %>)
|
|
83
|
+
<% else %> :bigint
|
|
84
|
+
<% end %> end
|
|
85
|
+
|
|
86
|
+
def key_type_of(table, column)
|
|
87
|
+
existing = table_exists?(table) && connection.columns(table).find { |c| c.name == column.to_s }
|
|
88
|
+
existing ? existing.sql_type : :bigint
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def add_column_once(table, column, type)
|
|
92
|
+
add_column table, column, type unless column_exists?(table, column)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def add_index_once(table, columns, **options)
|
|
96
|
+
add_index table, columns, **options unless index_name_exists?(table, options[:name])
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
class PermissionForgeAddResourceFieldsUniqueIndex < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
def up
|
|
3
|
+
table = :<%= table_prefix %>resource_fields
|
|
4
|
+
index_name = 'idx_pf_resource_fields_unique'
|
|
5
|
+
columns = %i[resource_id name]
|
|
6
|
+
# Installs from 0.11 on create this unique index in their install
|
|
7
|
+
# migration: there is nothing to do. A same-named index that is not unique
|
|
8
|
+
# is replaced (add_index would fail on the existing name).
|
|
9
|
+
return if index_exists?(table, columns, name: index_name, unique: true)
|
|
10
|
+
|
|
11
|
+
remove_index table, name: index_name if index_name_exists?(table, index_name)
|
|
12
|
+
|
|
13
|
+
# Collapse any pre-existing duplicate (resource_id, name) rows before adding
|
|
14
|
+
# the unique index, otherwise index creation fails. Keep the most-restrictive
|
|
15
|
+
# row per group: masked over unmasked, then no-edit over edit, then lowest id.
|
|
16
|
+
duplicate_groups = select_rows(<<~SQL.squish)
|
|
17
|
+
SELECT resource_id, name, COUNT(*) AS c
|
|
18
|
+
FROM #{quote_table_name(table)}
|
|
19
|
+
GROUP BY resource_id, name
|
|
20
|
+
HAVING COUNT(*) > 1
|
|
21
|
+
SQL
|
|
22
|
+
|
|
23
|
+
duplicate_groups.each do |resource_id, name, _count|
|
|
24
|
+
rows = select_rows(<<~SQL.squish)
|
|
25
|
+
SELECT id, mask_access, edit_access
|
|
26
|
+
FROM #{quote_table_name(table)}
|
|
27
|
+
WHERE resource_id = #{quote(resource_id)}
|
|
28
|
+
AND name = #{quote(name)}
|
|
29
|
+
SQL
|
|
30
|
+
|
|
31
|
+
keeper = rows.min_by { |id, mask_access, edit_access| [truthy(mask_access) ? 0 : 1, truthy(edit_access) ? 1 : 0, id] }
|
|
32
|
+
loser_ids = rows.map(&:first) - [keeper.first]
|
|
33
|
+
next if loser_ids.empty?
|
|
34
|
+
|
|
35
|
+
# These deletes run past the models, so the audit log does not record
|
|
36
|
+
# them: the migration output is their trace.
|
|
37
|
+
say "resource_id #{resource_id}, name #{name.to_s.inspect}: kept id #{keeper[0]} " \
|
|
38
|
+
"(mask_access #{truthy(keeper[1])}, edit_access #{truthy(keeper[2])}); " \
|
|
39
|
+
"deleted ids #{loser_ids.sort.join(', ')} (not in the audit log)"
|
|
40
|
+
|
|
41
|
+
execute(<<~SQL.squish)
|
|
42
|
+
DELETE FROM #{quote_table_name(table)}
|
|
43
|
+
WHERE id IN (#{loser_ids.map { |id| quote(id) }.join(',')})
|
|
44
|
+
SQL
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
add_index table, columns, unique: true, name: index_name
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# On an install from 0.11 on, `up` changed nothing, yet this removes the
|
|
51
|
+
# index the install migration created (as it always has).
|
|
52
|
+
def down
|
|
53
|
+
remove_index :<%= table_prefix %>resource_fields, name: 'idx_pf_resource_fields_unique'
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def truthy(value)
|
|
59
|
+
[true, 1, '1', 't', 'true'].include?(value)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def select_rows(sql)
|
|
63
|
+
connection.select_rows(sql)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def quote(value)
|
|
67
|
+
connection.quote(value)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
require 'rails/generators'
|
|
2
|
+
require 'rails/generators/active_record'
|
|
3
|
+
|
|
4
|
+
module PermissionForge
|
|
5
|
+
module Generators
|
|
6
|
+
# Adds schema hardening that shipped after the initial install, for apps that
|
|
7
|
+
# ran `permission_forge:install` before these guards existed.
|
|
8
|
+
#
|
|
9
|
+
# bin/rails generate permission_forge:upgrade
|
|
10
|
+
#
|
|
11
|
+
# The table prefix defaults to the app's configured
|
|
12
|
+
# PermissionForge.table_name_prefix (the initializer is loaded by
|
|
13
|
+
# `rails generate`), so an app on unprefixed tables gets migrations for
|
|
14
|
+
# its real tables. Pass --table-prefix to override.
|
|
15
|
+
#
|
|
16
|
+
# Currently: de-duplicates any (resource_id, name) collisions in
|
|
17
|
+
# resource_fields (keeping the most-restrictive row) and adds the unique
|
|
18
|
+
# index that new installs get by default; adds the nullable
|
|
19
|
+
# resource_fields.sensitive_acknowledged_at column used by the admin UI's
|
|
20
|
+
# privacy audit ("This is intended"); creates the audit_events table and
|
|
21
|
+
# the review/reason columns (see add_audit_events.rb.tt).
|
|
22
|
+
class UpgradeGenerator < Rails::Generators::Base
|
|
23
|
+
include ActiveRecord::Generators::Migration
|
|
24
|
+
|
|
25
|
+
source_root File.expand_path('templates', __dir__)
|
|
26
|
+
|
|
27
|
+
class_option :table_prefix, type: :string, default: nil,
|
|
28
|
+
desc: 'Table name prefix (default: the configured PermissionForge.table_name_prefix)'
|
|
29
|
+
class_option :tenant_column, type: :string, default: nil,
|
|
30
|
+
desc: 'Tenant column of the audit table (default: the configured tenant column, if any)'
|
|
31
|
+
|
|
32
|
+
def copy_migration
|
|
33
|
+
migration_template 'add_resource_fields_unique_index.rb.tt',
|
|
34
|
+
'db/migrate/permission_forge_add_resource_fields_unique_index.rb'
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Admin UI privacy audit: "This is intended" acknowledgements.
|
|
38
|
+
def copy_admin_ui_migration
|
|
39
|
+
migration_template 'add_admin_ui_columns.rb.tt',
|
|
40
|
+
'db/migrate/permission_forge_add_admin_ui_columns.rb'
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Insert-only audit log (permission_forge_audit_events) and the columns
|
|
44
|
+
# the access review and acknowledgement record. Guarded: safe to re-run.
|
|
45
|
+
def copy_audit_events_migration
|
|
46
|
+
migration_template 'add_audit_events.rb.tt',
|
|
47
|
+
'db/migrate/permission_forge_add_audit_events.rb'
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def table_prefix
|
|
53
|
+
options[:table_prefix] || PermissionForge.table_name_prefix.to_s
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# The tenant column of the app's other tables, or nil without tenancy.
|
|
57
|
+
def tenant_column
|
|
58
|
+
options[:tenant_column].presence || PermissionForge.configuration.resolved_tenant_column&.to_s
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def migration_version
|
|
62
|
+
"[#{ActiveRecord::Migration.current_version}]"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
# One resource's action rows in every Permission Set that contains the
|
|
6
|
+
# resource: who manages an action and who permits it. Loaded on first use,
|
|
7
|
+
# once for all of the resource's action keys. Backs the Actions tab, the
|
|
8
|
+
# action drawers ("Across all Permission Sets") and the "Review and save"
|
|
9
|
+
# sentences.
|
|
10
|
+
#
|
|
11
|
+
# "Managed" means what ControllerActions::ManagedIndex means: a row in
|
|
12
|
+
# effect (ResourceAction#in_effect?) in any Permission Set, granting or
|
|
13
|
+
# muting. This reads the database, not the index, so it also knows *which*
|
|
14
|
+
# sets.
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# across = ActionAcrossSets.new('Customer')
|
|
18
|
+
# across.managed?('export') # => true
|
|
19
|
+
# across.permitting_sets('export') # => [#<PermissionSet name: "Sales rep">]
|
|
20
|
+
class ActionAcrossSets
|
|
21
|
+
def initialize(resource_name)
|
|
22
|
+
@resource_name = resource_name.to_s
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @param key [String, Symbol] action key (e.g. 'export')
|
|
26
|
+
# @return [Array<Hash>] { resource:, set:, action: (ResourceAction or nil), in_effect: }
|
|
27
|
+
# for every set containing the resource — granting sets first, then by
|
|
28
|
+
# name. in_effect is whether the row (or, without a row, the resource)
|
|
29
|
+
# counts right now.
|
|
30
|
+
def rows(key)
|
|
31
|
+
name = key.to_s
|
|
32
|
+
resources.map do |resource|
|
|
33
|
+
action = action_in(resource, name)
|
|
34
|
+
{ resource: resource, set: resource.permission_set, action: action, in_effect: (action || resource).in_effect? }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @return [Boolean] whether any Permission Set manages the action right now
|
|
39
|
+
def managed?(key)
|
|
40
|
+
live_rows.key?(key.to_s)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @return [Array<PermissionForge::PermissionSet>] granting sets whose row
|
|
44
|
+
# permits the action right now
|
|
45
|
+
def permitting_sets(key)
|
|
46
|
+
live_rows.fetch(key.to_s, []).filter_map do |permission_set, action|
|
|
47
|
+
permission_set if action.is_permitted && !permission_set.is_muting
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def resources
|
|
54
|
+
@resources ||= PermissionForge::Resource.where(name: @resource_name).includes(:permission_set, :resource_actions)
|
|
55
|
+
.sort_by { |resource| sort_key(resource.permission_set) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def sort_key(permission_set)
|
|
59
|
+
[permission_set.is_muting ? 1 : 0, permission_set.name]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# { 'export' => [[PermissionSet, ResourceAction], ...] } — the rows in effect.
|
|
63
|
+
def live_rows
|
|
64
|
+
@live_rows ||= resources.each_with_object({}) do |resource, index|
|
|
65
|
+
resource.resource_actions.select(&:in_effect?).each do |action|
|
|
66
|
+
(index[action.name] ||= []) << [resource.permission_set, action]
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def action_in(resource, name)
|
|
72
|
+
resource.resource_actions.find { |action| action.name == name }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
# One row per action on a Resource page: configured ResourceActions and —
|
|
6
|
+
# while controller_action_discovery is on — the app's controller actions
|
|
7
|
+
# that map to the resource but have no row in this set yet.
|
|
8
|
+
#
|
|
9
|
+
# Each row carries where the action runs (endpoints, with whether that
|
|
10
|
+
# controller runs the action guard) and what the saved configuration
|
|
11
|
+
# means right now for people with this set (effect).
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# rows = ActionRows.new(resource)
|
|
15
|
+
# rows.configured_rows.map(&:name) # => ['export']
|
|
16
|
+
# rows.discovered_rows.map(&:name) # => ['archive', 'merge']
|
|
17
|
+
# rows.row('archive').effect # => :unmanaged_allowed
|
|
18
|
+
class ActionRows
|
|
19
|
+
# endpoints: ControllerCatalog::Entry list (empty while discovery is off).
|
|
20
|
+
# unlinked: configured, but no controller action maps to it — normal for
|
|
21
|
+
# an action the app checks in code with can?.
|
|
22
|
+
# standing: what the row means right now (see Standing).
|
|
23
|
+
Row = Struct.new(:name, :label, :action, :endpoints, :unlinked, :standing, keyword_init: true) do
|
|
24
|
+
delegate :effect, :permitted_by, to: :standing
|
|
25
|
+
|
|
26
|
+
def configured? = !action.nil?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# What one action means right now for people with this set, and which
|
|
30
|
+
# sets permit it. The other Permission Sets are only read when a row is
|
|
31
|
+
# asked for it: the Actions tab shows it only while discovery is on.
|
|
32
|
+
#
|
|
33
|
+
# effect:
|
|
34
|
+
# :permitted this set permits it
|
|
35
|
+
# :not_permitted this set has a row that does not permit it
|
|
36
|
+
# :blocked this muting set blocks it
|
|
37
|
+
# :unchanged this muting set has a row that does not block it
|
|
38
|
+
# :managed_elsewhere no row in effect here, and another set manages it
|
|
39
|
+
# :unmanaged_allowed no set manages it — everyone can run it
|
|
40
|
+
# :unmanaged_blocked no set manages it — blocked until a set permits it
|
|
41
|
+
class Standing
|
|
42
|
+
# @param action [PermissionForge::ResourceAction, nil] this set's row
|
|
43
|
+
def initialize(key, action, across:, configuration:)
|
|
44
|
+
@key = key
|
|
45
|
+
@action = action
|
|
46
|
+
@across = across
|
|
47
|
+
@configuration = configuration
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def effect
|
|
51
|
+
@effect ||= @action&.in_effect? ? row_effect : effect_without_row
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @return [Array<PermissionForge::PermissionSet>] granting sets that
|
|
55
|
+
# permit the action right now
|
|
56
|
+
def permitted_by
|
|
57
|
+
@permitted_by ||= @across.permitting_sets(@key)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
# This set's row is in effect. In a muting set is_permitted means "block".
|
|
63
|
+
def row_effect
|
|
64
|
+
flagged = @action.is_permitted
|
|
65
|
+
if @action.resource.permission_set.is_muting
|
|
66
|
+
flagged ? :blocked : :unchanged
|
|
67
|
+
else
|
|
68
|
+
flagged ? :permitted : :not_permitted
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# No row in effect here: another set manages it, or nobody does.
|
|
73
|
+
def effect_without_row
|
|
74
|
+
return :managed_elsewhere if @across.managed?(@key)
|
|
75
|
+
|
|
76
|
+
@configuration.allow_unconfigured_actions? ? :unmanaged_allowed : :unmanaged_blocked
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def initialize(resource, catalog: nil, configuration: PermissionForge.configuration,
|
|
81
|
+
across: ActionAcrossSets.new(resource.name))
|
|
82
|
+
@resource = resource
|
|
83
|
+
@catalog = catalog
|
|
84
|
+
@configuration = configuration
|
|
85
|
+
@across = across
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @return [Boolean] whether the app's controller actions are listed
|
|
89
|
+
def discovery?
|
|
90
|
+
@configuration.controller_action_discovery?
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# @return [Array<Row>] configured rows, then discovered ones, each by key
|
|
94
|
+
def rows
|
|
95
|
+
@rows ||= configured_rows + discovered_rows
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def configured_rows
|
|
99
|
+
@configured_rows ||= saved_actions.sort_by(&:name).map { |action| build_row(action.name, action) }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Controller actions that map to the resource and have no row in this set.
|
|
103
|
+
def discovered_rows
|
|
104
|
+
@discovered_rows ||= (endpoints_by_key.keys - configured_names).sort.map { |key| build_row(key, nil) }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def row(name)
|
|
108
|
+
key = name.to_s
|
|
109
|
+
rows.find { |row| row.name == key }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Discovered keys with no row yet, offered on the free-text "new action" form.
|
|
113
|
+
def suggestions
|
|
114
|
+
discovered_rows.map(&:name)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# @return [Boolean] whether any of the app's listed controllers runs the
|
|
118
|
+
# action guard (false: nothing here is enforced at runtime)
|
|
119
|
+
def any_guarded?
|
|
120
|
+
catalog.any_guarded?
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# This action (by key) in every Permission Set containing the resource.
|
|
124
|
+
def across_sets(key)
|
|
125
|
+
@across.rows(key)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
private
|
|
129
|
+
|
|
130
|
+
def catalog
|
|
131
|
+
@catalog ||= ControllerCatalog.new(configuration: @configuration)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Rows a form built on the association but has not saved are not configured.
|
|
135
|
+
def saved_actions
|
|
136
|
+
@resource.resource_actions.select(&:persisted?)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def configured_names
|
|
140
|
+
saved_actions.map(&:name)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# { 'export' => [ControllerCatalog::Entry, ...] }; empty while discovery is off.
|
|
144
|
+
def endpoints_by_key
|
|
145
|
+
@endpoints_by_key ||= discovery? ? catalog.for_resource(@resource.name).group_by(&:key) : {}
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def build_row(key, action)
|
|
149
|
+
endpoints = endpoints_by_key.fetch(key, [])
|
|
150
|
+
Row.new(name: key, label: action&.display_name || key.humanize, action: action, endpoints: endpoints,
|
|
151
|
+
unlinked: discovery? && endpoints.empty?,
|
|
152
|
+
standing: Standing.new(key, action, across: @across, configuration: @configuration))
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Admin
|
|
5
|
+
# Everything the admin home page asks the owner to look at. Each finder is
|
|
6
|
+
# a read-only query; actions (extend, keep, clean up, acknowledge) live in
|
|
7
|
+
# the controllers.
|
|
8
|
+
class AttentionItems
|
|
9
|
+
ENDING_DAYS = 7
|
|
10
|
+
|
|
11
|
+
# `field` — the field row, when there is one, switched on or off.
|
|
12
|
+
# `implicit_grant` — no active field row, and the field is readable and
|
|
13
|
+
# editable anyway because the model declares it as an extra (virtual) field.
|
|
14
|
+
SensitiveFinding = Struct.new(:permission_set, :resource, :field, :column, :match, :people_count, :implicit_grant,
|
|
15
|
+
keyword_init: true) do
|
|
16
|
+
# No setting in force: no row, or one that is turned off (the engine skips it).
|
|
17
|
+
def unconfigured? = field.nil? || turned_off?
|
|
18
|
+
def turned_off? = !field.nil? && !field.is_active
|
|
19
|
+
def implicit_grant? = implicit_grant == true
|
|
20
|
+
def name = field&.name || column
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
TidyFinding = Struct.new(:permission_set, :kind, :people_count, keyword_init: true) do
|
|
24
|
+
# A finding for an active set nobody uses, or an inactive set people
|
|
25
|
+
# are still assigned to; nil otherwise.
|
|
26
|
+
def self.for(permission_set, people)
|
|
27
|
+
active = permission_set.is_active
|
|
28
|
+
if people.zero? && active
|
|
29
|
+
new(permission_set: permission_set, kind: :no_people, people_count: 0)
|
|
30
|
+
elsif people.positive? && !active
|
|
31
|
+
new(permission_set: permission_set, kind: :inactive_with_people, people_count: people)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Active record rules the app's query scoping won't apply to every query:
|
|
37
|
+
# all of them while auto_query_scoping is off, join rules when it's on.
|
|
38
|
+
RulesFinding = Struct.new(:permission_set, :resource, :rule_count, :people_count, keyword_init: true) do
|
|
39
|
+
def self.build(resource, rule_count, people_counts)
|
|
40
|
+
new(permission_set: resource.permission_set, resource: resource, rule_count: rule_count,
|
|
41
|
+
people_count: people_counts.fetch(resource.permission_set_id, 0))
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.acknowledgement_supported?
|
|
46
|
+
PermissionForge::ResourceField.column_names.include?('sensitive_acknowledged_at')
|
|
47
|
+
rescue StandardError
|
|
48
|
+
false
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def initialize(configuration: PermissionForge.configuration, catalog: ModelCatalog.new,
|
|
52
|
+
sensitive: SensitiveColumns.new(configuration: configuration))
|
|
53
|
+
@configuration = configuration
|
|
54
|
+
@catalog = catalog
|
|
55
|
+
@sensitive = sensitive
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def total
|
|
59
|
+
[audit_findings.size, privacy_findings.size, unenforced_rules.size, ending_soon.size, expired.size, stale.size,
|
|
60
|
+
tidy.size].sum
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def all_good?
|
|
64
|
+
total.zero?
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# --- Audit log -----------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
# [AuditLog.recording_problem] — :off, :failing or :missing_table —
|
|
70
|
+
# while the log does not record. Empty when it records.
|
|
71
|
+
def audit_findings
|
|
72
|
+
@audit_findings ||= [audit_problem].compact
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# --- Privacy audit ------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
def privacy_findings
|
|
78
|
+
@privacy_findings ||= unmasked_sensitive_fields + unconfigured_sensitive_fields
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# --- Record rules --------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
def unenforced_rules
|
|
84
|
+
@unenforced_rules ||= begin
|
|
85
|
+
enforced = rule_enforced_resources
|
|
86
|
+
counts = unenforced_rule_scope.where(resource_id: enforced.keys).group(:resource_id).count
|
|
87
|
+
findings = counts.map { |id, count| RulesFinding.build(enforced.fetch(id), count, people_counts) }
|
|
88
|
+
findings.sort_by { |finding| [finding.permission_set.name, finding.resource.name] }
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def auto_query_scoping?
|
|
93
|
+
@configuration.auto_query_scoping_enabled?
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# --- Assignments --------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
def ending_soon
|
|
99
|
+
@ending_soon ||= assignments.where(expires_at: Time.current..ENDING_DAYS.days.from_now.end_of_day)
|
|
100
|
+
.order(:expires_at).to_a
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def expired
|
|
104
|
+
@expired ||= assignments.expired.order(:expires_at).to_a
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def stale
|
|
108
|
+
@stale ||= assignments.stale_before(stale_days.days.ago)
|
|
109
|
+
.order(PermissionForge::PermissionSetUser.last_reviewed_column).to_a
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def stale_days
|
|
113
|
+
@configuration.admin_stale_assignment_days || 90
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# --- Permission Sets to tidy up -----------------------------------------
|
|
117
|
+
|
|
118
|
+
def tidy
|
|
119
|
+
@tidy ||= PermissionForge::PermissionSet.order(:name).filter_map do |ps|
|
|
120
|
+
TidyFinding.for(ps, people_counts.fetch(ps.id, 0))
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
def audit_problem = AuditLog.recording_problem(@configuration)
|
|
127
|
+
|
|
128
|
+
def assignments
|
|
129
|
+
PermissionForge::PermissionSetUser.includes(:permission_set, :user)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Resources whose rules are in force (View All / Modify All skip them), by id.
|
|
133
|
+
def rule_enforced_resources
|
|
134
|
+
granting_resources.reject(&:rules_bypassed?).index_by(&:id)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def unenforced_rule_scope
|
|
138
|
+
rules = PermissionForge::PermissionSetRule.active
|
|
139
|
+
auto_query_scoping? ? rules.join_type_rules : rules
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def people_counts
|
|
143
|
+
@people_counts ||= PermissionForge::PermissionSetUser.active_assignments.group(:permission_set_id).count
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def granting_resources
|
|
147
|
+
@granting_resources ||= PermissionForge::Resource.active.where(read_access: true)
|
|
148
|
+
.where(permission_set_id: PermissionForge::PermissionSet.active.granting.select(:id))
|
|
149
|
+
.includes(:permission_set, :resource_fields).to_a
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def unmasked_sensitive_fields
|
|
153
|
+
acknowledged = self.class.acknowledgement_supported?
|
|
154
|
+
granting_resources.flat_map do |resource|
|
|
155
|
+
resource.resource_fields.filter_map do |field|
|
|
156
|
+
sensitive_finding(resource, field.name, field: field) if exposed?(field, acknowledged)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Readable and unmasked, and not acknowledged as intentional.
|
|
162
|
+
def exposed?(field, acknowledged)
|
|
163
|
+
return false unless field.is_active && field.read_access && !field.mask_access
|
|
164
|
+
|
|
165
|
+
!(acknowledged && field.sensitive_acknowledged_at.present?)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Sensitive columns and virtual fields with no active field row are a
|
|
169
|
+
# leak when they are readable unmasked anyway: the unconfigured-field
|
|
170
|
+
# strategy lets them through, or the model declares them as extra fields
|
|
171
|
+
# and extra_fields_default_access grants those. A switched-off row
|
|
172
|
+
# counts as no row, because the engine skips it; its finding keeps the
|
|
173
|
+
# row, so the card can say "turned off" instead of "not configured".
|
|
174
|
+
def unconfigured_sensitive_fields
|
|
175
|
+
passes = %w[pass_through warn].include?(@configuration.unconfigured_field_strategy.to_s)
|
|
176
|
+
return [] unless passes || @configuration.grant_extra_fields_by_default?
|
|
177
|
+
|
|
178
|
+
granting_resources.flat_map { |resource| unconfigured_findings(resource, passes) }
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# `passes` — whether unconfigured_field_strategy lets a field with no row through.
|
|
182
|
+
def unconfigured_findings(resource, passes)
|
|
183
|
+
model = @catalog.find(resource.name)
|
|
184
|
+
return [] unless model
|
|
185
|
+
|
|
186
|
+
rows = resource.resource_fields.index_by(&:name)
|
|
187
|
+
model.managed_field_names.filter_map do |name|
|
|
188
|
+
row = rows[name]
|
|
189
|
+
next if row&.is_active
|
|
190
|
+
|
|
191
|
+
implicit = model.implicit_grant?(name)
|
|
192
|
+
sensitive_finding(resource, name, field: row, implicit_grant: implicit) if implicit || passes
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# A finding when `column` looks sensitive, else nil. `details` are the
|
|
197
|
+
# finding's own attributes (field:, implicit_grant:).
|
|
198
|
+
def sensitive_finding(resource, column, **details)
|
|
199
|
+
match = @sensitive.match(column)
|
|
200
|
+
return unless match
|
|
201
|
+
|
|
202
|
+
SensitiveFinding.new(permission_set: resource.permission_set, resource: resource, column: column, match: match,
|
|
203
|
+
people_count: people_counts.fetch(resource.permission_set_id, 0), **details)
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|