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,175 @@
|
|
|
1
|
+
module PermissionForge
|
|
2
|
+
# Plain-language sentences for activation conditions and record rules
|
|
3
|
+
# (audit events: AuditSentencesHelper; "Review and save" change
|
|
4
|
+
# descriptions: ChangesHelper).
|
|
5
|
+
#
|
|
6
|
+
# @api private
|
|
7
|
+
module SentencesHelper
|
|
8
|
+
NULLABLE_OPERATORS = %w[equal not_equal in not_in].freeze
|
|
9
|
+
LIST_OPERATORS = %w[in not_in].freeze
|
|
10
|
+
NO_VALUE_OPERATORS = %w[is_null is_not_null].freeze
|
|
11
|
+
USER_OPERATORS = %w[equal not_equal in not_in is_null is_not_null].freeze
|
|
12
|
+
CURRENT_USER = 'current_user'.freeze
|
|
13
|
+
|
|
14
|
+
# --- activation conditions ------------------------------------------------
|
|
15
|
+
|
|
16
|
+
def pf_condition_parts(permission_set)
|
|
17
|
+
[pf_condition_ips(permission_set), pf_condition_hours(permission_set), pf_condition_flag(permission_set)].compact
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def pf_condition_ips(permission_set)
|
|
21
|
+
ips = pf_ip_list(permission_set.allowed_ips)
|
|
22
|
+
return if ips.empty?
|
|
23
|
+
|
|
24
|
+
connector = pf_ui('conditions.or')
|
|
25
|
+
pf_ui('conditions.from_ips', ips: ips.to_sentence(two_words_connector: connector, last_word_connector: connector))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def pf_condition_hours(permission_set)
|
|
29
|
+
from = permission_set.allowed_hours_start
|
|
30
|
+
to = permission_set.allowed_hours_end
|
|
31
|
+
return unless from.present? && to.present?
|
|
32
|
+
|
|
33
|
+
key = to.to_i <= from.to_i ? 'hours_overnight' : 'hours'
|
|
34
|
+
pf_ui("conditions.#{key}", from: pf_hour(from), to: pf_hour(to))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def pf_condition_flag(permission_set)
|
|
38
|
+
flag = permission_set.requires_session_flag
|
|
39
|
+
pf_flag_phrase(flag) if flag.present?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def pf_condition_sentence(permission_set)
|
|
43
|
+
parts = pf_condition_parts(permission_set)
|
|
44
|
+
return pf_ui('conditions.always') if parts.empty?
|
|
45
|
+
|
|
46
|
+
pf_ui('conditions.only_works', parts: parts.join(', '))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def pf_ip_list(value)
|
|
50
|
+
value.to_s.split(/[,\n]/).map(&:strip).reject(&:empty?)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def pf_flag_label(flag)
|
|
54
|
+
configured = PermissionForge.configuration.admin_session_flags || {}
|
|
55
|
+
configured[flag.to_s] || configured[flag.to_sym] ||
|
|
56
|
+
t("permission_forge.ui.flags.#{flag}", default: flag.to_s.humanize)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def pf_flag_phrase(flag)
|
|
60
|
+
t("permission_forge.ui.conditions.flags.#{flag}",
|
|
61
|
+
default: pf_ui('conditions.flag_generic', flag: pf_flag_label(flag)))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# --- record rules ---------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
def pf_operator_label(operator)
|
|
67
|
+
t("permission_forge.ui.operators.#{operator}", default: operator.to_s.humanize.downcase)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Labels along a rule's relationship path and the final field, e.g.
|
|
71
|
+
# ["Organization", "Members", "User"].
|
|
72
|
+
def pf_rule_field_labels(model_name, rule)
|
|
73
|
+
klass = model_name.to_s.safe_constantize
|
|
74
|
+
labels = rule.field_accessor_path.to_s.split('.').map do |assoc|
|
|
75
|
+
label = pf_attribute_label(klass, assoc)
|
|
76
|
+
klass = pf_association_class(klass, assoc)
|
|
77
|
+
label
|
|
78
|
+
end
|
|
79
|
+
labels << pf_attribute_label(klass, rule.field_name)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def pf_attribute_label(klass, attribute)
|
|
83
|
+
klass ? klass.human_attribute_name(attribute) : attribute.to_s.humanize
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Target class of an association, or nil (unknown or polymorphic).
|
|
87
|
+
def pf_association_class(klass, assoc)
|
|
88
|
+
reflection = klass&.reflect_on_association(assoc.to_sym)
|
|
89
|
+
begin
|
|
90
|
+
reflection&.klass
|
|
91
|
+
rescue StandardError
|
|
92
|
+
nil
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def pf_rule_value_text(rule)
|
|
97
|
+
value = rule.field_value.to_s
|
|
98
|
+
return '…' if value.strip.empty? # builder preview before a value is entered
|
|
99
|
+
return pf_ui('rules.current_user') if value == CURRENT_USER
|
|
100
|
+
|
|
101
|
+
pf_rule_value_parts(rule.operator, value)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# List values as "a, b"; ranges as "a and b"; anything else verbatim.
|
|
105
|
+
def pf_rule_value_parts(operator, value)
|
|
106
|
+
return value.split(',').map(&:strip).join(', ') if LIST_OPERATORS.include?(operator)
|
|
107
|
+
return value.split(',', 2).map(&:strip).join(" #{pf_ui('rules.and')} ") if operator == 'between'
|
|
108
|
+
|
|
109
|
+
value
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def pf_rule_nullable?(rule)
|
|
113
|
+
rule.include_nulls && NULLABLE_OPERATORS.include?(rule.operator)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Plural model label. model_name.human(count: 2) falls back to the singular
|
|
117
|
+
# when the host app has no model translations, so pluralize English then.
|
|
118
|
+
def pf_resource_plural(name)
|
|
119
|
+
klass = name.to_s.safe_constantize
|
|
120
|
+
return name.to_s.demodulize.pluralize unless klass.respond_to?(:model_name)
|
|
121
|
+
|
|
122
|
+
plural = klass.model_name.human(count: 2)
|
|
123
|
+
plural == klass.model_name.human && I18n.locale.to_s == 'en' ? plural.pluralize : plural
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def pf_resource_label(resource_or_name)
|
|
127
|
+
name = resource_or_name.respond_to?(:name) ? resource_or_name.name : resource_or_name.to_s
|
|
128
|
+
return resource_or_name.label if resource_or_name.respond_to?(:label) && resource_or_name.label.present?
|
|
129
|
+
|
|
130
|
+
klass = name.safe_constantize
|
|
131
|
+
klass.respond_to?(:model_name) ? klass.model_name.human : name.demodulize.titleize
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Token sentence: Only show [Customers] where [Owner] [is] [the current user], or Owner is empty
|
|
135
|
+
def pf_rule_sentence(rule, model_name = rule.resource&.name)
|
|
136
|
+
field = pf_rule_field_labels(model_name, rule).join(' › ')
|
|
137
|
+
sentence = safe_join(pf_rule_tokens(rule, model_name, field), ' ')
|
|
138
|
+
sentence = safe_join([sentence, content_tag(:span, ", #{pf_ui('rules.or_empty', field: field)}")]) if pf_rule_nullable?(rule)
|
|
139
|
+
content_tag(:p, sentence, class: 'pf-sentence')
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def pf_rule_tokens(rule, model_name, field)
|
|
143
|
+
[
|
|
144
|
+
content_tag(:span, pf_ui('rules.only_show')),
|
|
145
|
+
content_tag(:span, pf_resource_plural(model_name), class: 'pf-token'),
|
|
146
|
+
content_tag(:span, pf_ui('rules.where')),
|
|
147
|
+
content_tag(:span, field, class: 'pf-token'),
|
|
148
|
+
content_tag(:span, pf_operator_label(rule.operator), class: 'pf-token'),
|
|
149
|
+
pf_rule_value_token(rule)
|
|
150
|
+
].compact
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def pf_rule_value_token(rule)
|
|
154
|
+
return if NO_VALUE_OPERATORS.include?(rule.operator)
|
|
155
|
+
|
|
156
|
+
content_tag(:span, pf_rule_value_text(rule), class: 'pf-token pf-token--value')
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def pf_rule_text(rule, model_name = rule.resource&.name)
|
|
160
|
+
field = pf_rule_field_labels(model_name, rule).join(' › ')
|
|
161
|
+
text = "#{pf_ui('rules.only_show')} #{pf_resource_plural(model_name)} #{pf_ui('rules.where')} #{field} " \
|
|
162
|
+
"#{pf_operator_label(rule.operator)}"
|
|
163
|
+
text += " #{pf_rule_value_text(rule)}" unless NO_VALUE_OPERATORS.include?(rule.operator)
|
|
164
|
+
text += ", #{pf_ui('rules.or_empty', field: field)}" if pf_rule_nullable?(rule)
|
|
165
|
+
text
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Short form used in matrix cells: "Owner is the current user".
|
|
169
|
+
def pf_rule_summary(rule, model_name = rule.resource&.name)
|
|
170
|
+
field = pf_rule_field_labels(model_name, rule).join(' › ')
|
|
171
|
+
value = NO_VALUE_OPERATORS.include?(rule.operator) ? nil : pf_rule_value_text(rule)
|
|
172
|
+
[field, pf_operator_label(rule.operator), value].compact.join(' ')
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
module PermissionForge
|
|
2
|
+
# Renders configuration values on the read-only Settings page by type.
|
|
3
|
+
# Secrets show only "Set"/"Not set"; code hooks only default/custom/not set.
|
|
4
|
+
#
|
|
5
|
+
# @api private
|
|
6
|
+
module SettingsHelper
|
|
7
|
+
SECONDS_UNITS = [[86_400, :days], [3600, :hours], [60, :minutes]].freeze
|
|
8
|
+
|
|
9
|
+
# Current-value renderer per setting type (private methods taking the row);
|
|
10
|
+
# any other type renders as a scalar.
|
|
11
|
+
VALUE_RENDERERS = {
|
|
12
|
+
bool: :pf_setting_bool, enum: :pf_setting_enum, num: :pf_setting_number,
|
|
13
|
+
list: :pf_setting_list, names: :pf_setting_list, map: :pf_setting_map,
|
|
14
|
+
secret: :pf_setting_secret, hook: :pf_setting_hook
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
# Default-value text per setting type; any other type renders as a scalar.
|
|
18
|
+
DEFAULT_RENDERERS = {
|
|
19
|
+
bool: :pf_setting_default_bool, secret: :pf_setting_default_secret, hook: :pf_setting_default_hook,
|
|
20
|
+
list: :pf_setting_default_list, names: :pf_setting_default_list, map: :pf_setting_default_list,
|
|
21
|
+
num: :pf_setting_default_number
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
def pf_setting_value(row)
|
|
25
|
+
send(VALUE_RENDERERS.fetch(row.type, :pf_setting_scalar), row)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def pf_setting_default(row)
|
|
29
|
+
send(DEFAULT_RENDERERS.fetch(row.type, :pf_setting_default_scalar), row)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Compact "key = value" form for the health summary.
|
|
33
|
+
def pf_setting_inline_value(row)
|
|
34
|
+
case row.type
|
|
35
|
+
when :secret then row.value ? t('permission_forge.settings.values.set') : t('permission_forge.settings.values.not_set')
|
|
36
|
+
when :hook then t("permission_forge.settings.values.hook_#{row.hook_state}")
|
|
37
|
+
when :enum then ":#{row.value}"
|
|
38
|
+
else row.value.inspect
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def pf_setting_bool(row)
|
|
45
|
+
if row.value
|
|
46
|
+
content_tag(:span, "● #{t('permission_forge.settings.values.on')}", class: 'pf-pill pf-pill--on')
|
|
47
|
+
else
|
|
48
|
+
content_tag(:span, "○ #{t('permission_forge.settings.values.off')}", class: 'pf-pill')
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def pf_setting_enum(row)
|
|
53
|
+
options = Array(row.definition.options).map(&:to_s)
|
|
54
|
+
options << row.value unless options.include?(row.value.to_s)
|
|
55
|
+
pills = options.map do |opt|
|
|
56
|
+
current = opt == row.value.to_s
|
|
57
|
+
content_tag(:span, opt, class: "pf-pill pf-pill--opt#{' pf-pill--current' if current}", 'aria-current': (current ? 'true' : nil))
|
|
58
|
+
end
|
|
59
|
+
content_tag(:span, safe_join(pills, ' '), class: 'pf-row pf-row--tight')
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def pf_setting_number(row)
|
|
63
|
+
value = row.value
|
|
64
|
+
return content_tag(:span, t('permission_forge.settings.values.nil'), class: 'pf-text-3') if value.nil?
|
|
65
|
+
|
|
66
|
+
content_tag(:span, pf_setting_number_text(value, row.definition.unit), class: 'pf-tabular pf-fw-600')
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def pf_setting_number_text(value, unit)
|
|
70
|
+
return t('permission_forge.settings.values.nil') if value.nil?
|
|
71
|
+
return "#{value} #{t("permission_forge.settings.units.#{unit}", count: value)}" unless unit == :seconds
|
|
72
|
+
|
|
73
|
+
human = SECONDS_UNITS.find { |secs, _| (value % secs).zero? && value >= secs }
|
|
74
|
+
text = "#{value} s"
|
|
75
|
+
human ? "#{text} (#{t("permission_forge.settings.units.#{human.last}", count: value / human.first)})" : text
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def pf_setting_list(row)
|
|
79
|
+
value = row.value
|
|
80
|
+
return content_tag(:span, pf_setting_nil_text(row), class: 'pf-text-3') if value.nil?
|
|
81
|
+
return content_tag(:span, t('permission_forge.settings.values.empty'), class: 'pf-text-3') if value.empty?
|
|
82
|
+
|
|
83
|
+
content_tag(:span, safe_join(value.map { |v| content_tag(:code, v, class: 'pf-pill pf-pill--opt') }, ' '),
|
|
84
|
+
class: 'pf-row pf-row--tight')
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def pf_setting_map(row)
|
|
88
|
+
value = row.value
|
|
89
|
+
return content_tag(:span, t('permission_forge.settings.values.empty'), class: 'pf-text-3') if value.blank?
|
|
90
|
+
|
|
91
|
+
content_tag(:span, safe_join(value.map { |k, v| content_tag(:span, "#{k} → #{v}") }), class: 'pf-map')
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def pf_setting_secret(row)
|
|
95
|
+
if row.value
|
|
96
|
+
content_tag(:span, "● #{t('permission_forge.settings.values.set')}", class: 'pf-pill pf-pill--on')
|
|
97
|
+
else
|
|
98
|
+
content_tag(:span, "○ #{t('permission_forge.settings.values.not_set')} · #{t('permission_forge.settings.values.never_displayed')}",
|
|
99
|
+
class: 'pf-pill')
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def pf_setting_hook(row)
|
|
104
|
+
state = row.hook_state
|
|
105
|
+
css = state == :custom ? 'pf-pill pf-pill--on' : 'pf-pill'
|
|
106
|
+
content_tag(:span, t("permission_forge.settings.values.hook_#{state}"), class: css)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def pf_setting_scalar(row)
|
|
110
|
+
value = row.value
|
|
111
|
+
return content_tag(:span, pf_setting_nil_text(row), class: 'pf-text-3') if value.nil?
|
|
112
|
+
|
|
113
|
+
content_tag(:code, value.to_s, class: 'pf-mono')
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def pf_setting_default_bool(row)
|
|
117
|
+
row.default ? t('permission_forge.settings.values.on') : t('permission_forge.settings.values.off')
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def pf_setting_default_secret(_row)
|
|
121
|
+
t('permission_forge.settings.values.not_set')
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def pf_setting_default_hook(row)
|
|
125
|
+
t("permission_forge.settings.values.hook_#{row.default}")
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def pf_setting_default_list(row)
|
|
129
|
+
values = Array(row.default)
|
|
130
|
+
return t('permission_forge.settings.values.empty') if values.empty?
|
|
131
|
+
|
|
132
|
+
values.map { |v| Array(v).join(' → ') }.join(', ')
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def pf_setting_default_number(row)
|
|
136
|
+
pf_setting_number_text(row.default, row.definition.unit)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def pf_setting_default_scalar(row)
|
|
140
|
+
value = row.default
|
|
141
|
+
value.nil? ? pf_setting_nil_text(row) : value.to_s
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def pf_setting_nil_text(row)
|
|
145
|
+
case row.definition.nil_label
|
|
146
|
+
when :disabled then t('permission_forge.settings.values.disabled')
|
|
147
|
+
when :default then t('permission_forge.settings.values.hook_default')
|
|
148
|
+
else t('permission_forge.settings.values.nil')
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
module PermissionForge
|
|
2
|
+
# Building blocks shared by every admin screen: assets, text, dates, state
|
|
3
|
+
# cells, banners, footers, tabs, frames. Badges/chips live in BadgesHelper,
|
|
4
|
+
# people/assignment cells in PeopleHelper.
|
|
5
|
+
# Every class is pf- prefixed; no inline styles (strict-CSP friendly).
|
|
6
|
+
#
|
|
7
|
+
# @api private
|
|
8
|
+
module UiHelper
|
|
9
|
+
STATE_GLYPHS = { granted: '✓', blocked: '⊘', none: '–' }.freeze
|
|
10
|
+
|
|
11
|
+
# --- assets ---------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
def pf_asset_url(file)
|
|
14
|
+
permission_forge.pf_asset_path(file: file, v: PermissionForge::AssetsController.digest(file))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# ` nonce="…"` when the host app runs a CSP with nonces, else nothing.
|
|
18
|
+
def pf_nonce_attribute(_kind = :script)
|
|
19
|
+
nonce = respond_to?(:content_security_policy_nonce) ? content_security_policy_nonce : nil
|
|
20
|
+
nonce.present? ? tag.attributes(nonce: nonce) : ''
|
|
21
|
+
rescue StandardError
|
|
22
|
+
''
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def pf_lock_icon
|
|
26
|
+
tag.svg(class: 'pf-lock', viewBox: '0 0 16 16', fill: 'none', stroke: 'currentColor', 'stroke-width': '1.6',
|
|
27
|
+
'aria-hidden': 'true', focusable: 'false') do
|
|
28
|
+
safe_join([tag.rect(x: 3, y: 7, width: 10, height: 7, rx: 1.5), tag.path(d: 'M5.5 7V5a2.5 2.5 0 0 1 5 0v2')])
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# The attributes, only when the condition holds: `<input <%= pf_attrs_when(error, aria: { invalid: true }) %>>`.
|
|
33
|
+
# Built by `tag.attributes`, so every value is escaped; nothing is marked safe by hand.
|
|
34
|
+
def pf_attrs_when(condition, **attributes)
|
|
35
|
+
condition ? tag.attributes(**attributes) : ''
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# --- text helpers ---------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
def pf_ui(key, **)
|
|
41
|
+
t("permission_forge.ui.#{key}", **)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def pf_people_text(count)
|
|
45
|
+
pf_ui('people_count', count: count)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
MONTH_KEYS = %w[jan feb mar apr may jun jul aug sep oct nov dec].freeze
|
|
49
|
+
|
|
50
|
+
# "12 Mar 2026" / "12 Mar" using the engine's own month names (no
|
|
51
|
+
# dependency on rails-i18n being installed in the host app).
|
|
52
|
+
def pf_date(value, year: true)
|
|
53
|
+
return pf_ui('dates.never') if value.nil?
|
|
54
|
+
|
|
55
|
+
date = value.to_date
|
|
56
|
+
month = t("permission_forge.ui.months.#{MONTH_KEYS[date.month - 1]}")
|
|
57
|
+
year ? "#{date.day} #{month} #{date.year}" : "#{date.day} #{month}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def pf_datetime(value)
|
|
61
|
+
return pf_ui('dates.never') if value.nil?
|
|
62
|
+
|
|
63
|
+
"#{pf_date(value)} · #{value.in_time_zone.strftime('%H:%M')}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# "today", "yesterday", "3 days ago", or a date for anything older than 30 days.
|
|
67
|
+
def pf_relative(value)
|
|
68
|
+
return pf_ui('dates.never') if value.nil?
|
|
69
|
+
|
|
70
|
+
days = (Time.zone.today - value.to_date).to_i
|
|
71
|
+
return pf_ui('dates.today') if days <= 0
|
|
72
|
+
return pf_ui('dates.yesterday') if days == 1
|
|
73
|
+
return pf_ui('dates.days_ago', count: days) if days < 30
|
|
74
|
+
|
|
75
|
+
pf_date(value)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def pf_hour(hour)
|
|
79
|
+
format('%02d:00', hour.to_i)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Read-only state cell (effective access grid, across-sets lists).
|
|
83
|
+
def pf_state_cell(state, aria:, href: nil, why: nil, current: false)
|
|
84
|
+
state = state.to_sym
|
|
85
|
+
label = pf_ui("states.#{state}")
|
|
86
|
+
content = safe_join([content_tag(:span, STATE_GLYPHS.fetch(state), class: 'pf-cell__glyph', 'aria-hidden': true),
|
|
87
|
+
content_tag(:span, label)], ' ')
|
|
88
|
+
opts = { class: "pf-state pf-state--#{state}", 'aria-label': "#{aria}: #{label}" }
|
|
89
|
+
opts[:'aria-current'] = 'true' if current
|
|
90
|
+
opts[:data] = { pf_why: why } if why
|
|
91
|
+
href ? link_to(content, href, opts) : content_tag(:span, content, opts)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# --- banners & footers ----------------------------------------------------
|
|
95
|
+
|
|
96
|
+
def pf_help_banner(key, term:, text:)
|
|
97
|
+
return unless pf_help_visible?(key)
|
|
98
|
+
|
|
99
|
+
render 'permission_forge/shared/help_banner', key: key, term: term, text: text
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def pf_audit_footer(record, prefix: nil)
|
|
103
|
+
render 'permission_forge/shared/audit_footer', record: record, prefix: prefix
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def pf_tech_details(record, extras: {}, &block)
|
|
107
|
+
render 'permission_forge/shared/tech_details', record: record, extras: extras,
|
|
108
|
+
extra_html: (capture(&block) if block)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def pf_setting_path(key)
|
|
112
|
+
permission_forge.settings_path(focus: key, anchor: "set-#{key}")
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def pf_setting_link(key, text = nil)
|
|
116
|
+
link_to(text || pf_ui('open_setting'), pf_setting_path(key), class: 'pf-fs-12')
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Tab link with ARIA state, plus count.
|
|
120
|
+
def pf_tab(label, href, selected:, count: nil)
|
|
121
|
+
text = count.nil? ? label : "#{label} (#{count})"
|
|
122
|
+
link_to(text, href, class: 'pf-tab', role: 'tab', 'aria-selected': selected.to_s)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def pf_filter_chip(label, href, selected:, count: nil)
|
|
126
|
+
body = [content_tag(:span, label)]
|
|
127
|
+
body << content_tag(:span, count, class: 'pf-filter__count') unless count.nil?
|
|
128
|
+
link_to(safe_join(body), href, class: 'pf-filter', 'aria-current': (selected ? 'true' : nil))
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# A section heading inside a drawer or confirmation page. The page's own title is the h1 when
|
|
132
|
+
# the page stands alone and an h2 inside the dialog (the page behind has the h1), so a section
|
|
133
|
+
# is one level below that.
|
|
134
|
+
def pf_section_heading(text, id: nil)
|
|
135
|
+
content_tag(pf_fragment? ? :h3 : :h2, text, class: 'pf-h3', id: id)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Breadcrumb from [[label, path], ..., label]
|
|
139
|
+
def pf_breadcrumb(*items)
|
|
140
|
+
parts = items.map do |item|
|
|
141
|
+
item.is_a?(Array) ? link_to(item.first, item.last) : content_tag(:span, item, 'aria-current': 'page')
|
|
142
|
+
end
|
|
143
|
+
content_tag(:nav, safe_join(parts, content_tag(:span, '/', 'aria-hidden': true)),
|
|
144
|
+
class: 'pf-breadcrumb', 'aria-label': pf_ui('breadcrumb'))
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Re-emits nested params as hidden inputs (the review step re-posts the
|
|
148
|
+
# exact edits that were reviewed).
|
|
149
|
+
def pf_hidden_params(prefix, value)
|
|
150
|
+
value = value.to_unsafe_h if value.respond_to?(:to_unsafe_h)
|
|
151
|
+
case value
|
|
152
|
+
when Hash
|
|
153
|
+
safe_join(value.map { |k, v| pf_hidden_params("#{prefix}[#{k}]", v) })
|
|
154
|
+
when Array
|
|
155
|
+
safe_join(value.map { |v| pf_hidden_params("#{prefix}[]", v) })
|
|
156
|
+
when nil then safe_join([])
|
|
157
|
+
else
|
|
158
|
+
tag.input(type: 'hidden', name: prefix, value: value.to_s)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Drawer / dialog frames. The body is captured here (not via `render
|
|
163
|
+
# layout:`) so lazy t('.key') lookups inside it resolve against the
|
|
164
|
+
# calling view, not the frame partial.
|
|
165
|
+
def pf_sheet_frame(locals = {}, &)
|
|
166
|
+
render 'permission_forge/shared/sheet_frame', **locals, body: capture(&)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def pf_dialog_frame(locals = {}, &)
|
|
170
|
+
render 'permission_forge/shared/dialog_frame', **locals, body: capture(&)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Modal/drawer link: real page without JS, loaded into <dialog> with JS.
|
|
174
|
+
def pf_modal_link(text = nil, href = nil, kind: 'dialog', **opts, &block)
|
|
175
|
+
if block
|
|
176
|
+
opts = (href.is_a?(Hash) ? href : {}).merge(opts)
|
|
177
|
+
href = text
|
|
178
|
+
text = capture(&block)
|
|
179
|
+
end
|
|
180
|
+
opts[:data] = (opts[:data] || {}).merge(pf_modal: kind)
|
|
181
|
+
link_to(text, href, opts)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
module PermissionForge
|
|
2
|
+
# One audit event: who did what, to what, when, and what it was before.
|
|
3
|
+
# Written by PermissionForge::Audit only; insert-only once persisted
|
|
4
|
+
# (update, destroy and delete raise). The purge service deletes through a
|
|
5
|
+
# relation (`delete_all`), past the retention period, and records that.
|
|
6
|
+
#
|
|
7
|
+
# `data` is JSON text in the database and a Hash with string keys here:
|
|
8
|
+
# { "changes" => { attr => [from, to] } } on an update, { "snapshot" => {...} }
|
|
9
|
+
# on a create or destroy, plus event-specific keys.
|
|
10
|
+
#
|
|
11
|
+
# @api public
|
|
12
|
+
class AuditEvent < ActiveRecord::Base
|
|
13
|
+
# The table name, with the configured prefix (read on every call, so a prefix set at runtime applies).
|
|
14
|
+
def self.table_name = "#{PermissionForge.table_name_prefix}audit_events"
|
|
15
|
+
|
|
16
|
+
include PermissionForge::Concerns::HasTenantScoping
|
|
17
|
+
|
|
18
|
+
# A system event of a cross-tenant task (a rake clean-up, a report) has
|
|
19
|
+
# no tenant; it is visible while none is set, like any untenanted row.
|
|
20
|
+
def self.tenant_scoping_options
|
|
21
|
+
{ optional: true }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
CATEGORIES = PermissionForge::Audit::EventCatalog::CATEGORIES
|
|
25
|
+
ACTOR_TYPES = PermissionForge::Audit::Actor::TYPES
|
|
26
|
+
SOURCES = PermissionForge::Audit::Source::SOURCES
|
|
27
|
+
|
|
28
|
+
# JSON text that works on SQLite, PostgreSQL and MySQL alike; times are
|
|
29
|
+
# written in ISO 8601 (ActiveSupport::JSON), nothing reads as nil.
|
|
30
|
+
#
|
|
31
|
+
# @api private
|
|
32
|
+
class DataCoder
|
|
33
|
+
def self.dump(value)
|
|
34
|
+
ActiveSupport::JSON.encode(value.nil? ? {} : value)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.load(value)
|
|
38
|
+
value.blank? ? {} : ActiveSupport::JSON.decode(value)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
serialize :data, coder: DataCoder
|
|
43
|
+
|
|
44
|
+
# The tenant of the record the event is about. acts_as_tenant writes the
|
|
45
|
+
# ambient tenant to every new row (before_validation); an event follows
|
|
46
|
+
# its subject instead (before_create runs after validation).
|
|
47
|
+
attr_accessor :subject_tenant_id
|
|
48
|
+
|
|
49
|
+
before_validation :assign_uuid, on: :create
|
|
50
|
+
before_create :follow_subject_tenant
|
|
51
|
+
|
|
52
|
+
validates :uuid, :event, :category, :actor_type, :source, :occurred_at, presence: true
|
|
53
|
+
validates :category, inclusion: { in: CATEGORIES }
|
|
54
|
+
validates :actor_type, inclusion: { in: ACTOR_TYPES }
|
|
55
|
+
validates :source, inclusion: { in: SOURCES }
|
|
56
|
+
|
|
57
|
+
scope :newest_first, -> { order(occurred_at: :desc, id: :desc) }
|
|
58
|
+
scope :for_record, ->(record) { where(record_type: record.class.base_class.name, record_id: record.id.to_s) }
|
|
59
|
+
scope :for_subject, ->(user_id) { where(subject_user_id: user_id.to_s) }
|
|
60
|
+
scope :between, ->(from, to) { where(occurred_at: from..to) }
|
|
61
|
+
|
|
62
|
+
apply_tenant_scoping! # from the configuration, as the class loads (HasTenantScoping)
|
|
63
|
+
|
|
64
|
+
# The tenant column of the events: nil when tenancy is off or the table
|
|
65
|
+
# has no such column (installed before tenancy), where events cannot be
|
|
66
|
+
# told apart by tenant.
|
|
67
|
+
#
|
|
68
|
+
# @return [Symbol, nil]
|
|
69
|
+
def self.tenant_column
|
|
70
|
+
column = PermissionForge.configuration.resolved_tenant_column
|
|
71
|
+
column if column && column_names.include?(column.to_s)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# The events of one tenant, on the relation it is called on; every event
|
|
75
|
+
# when there is no tenant id or no tenant column. Used by the services
|
|
76
|
+
# that work past the default scope (`unscoped`): erasure, the purge.
|
|
77
|
+
#
|
|
78
|
+
# @param tenant_id [Object, nil]
|
|
79
|
+
# @return [ActiveRecord::Relation]
|
|
80
|
+
def self.in_tenant(tenant_id)
|
|
81
|
+
column = tenant_column
|
|
82
|
+
tenant_id && column ? where(column => tenant_id) : all
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# The one write (Audit::Recorder). Built under `unscoped`: instantiating
|
|
86
|
+
# a model that has a default scope evaluates it (Rails fills in scope
|
|
87
|
+
# attributes), and acts_as_tenant's raises under require_tenant when no
|
|
88
|
+
# tenant is ambient — a job, a rake task. The tenant comes from the
|
|
89
|
+
# subject (subject_tenant_id), else from the ambient tenant.
|
|
90
|
+
#
|
|
91
|
+
# @param attributes [Hash] columns plus subject_tenant_id:
|
|
92
|
+
# @return [AuditEvent] persisted
|
|
93
|
+
# @raise [ActiveRecord::RecordInvalid, ActiveRecord::StatementInvalid]
|
|
94
|
+
def self.store!(attributes)
|
|
95
|
+
event = unscoped { new(attributes) }
|
|
96
|
+
event.save!
|
|
97
|
+
event
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Insert-only. On a persisted event these raise ActiveRecord::ReadOnlyRecord:
|
|
101
|
+
# save, save!, update, update!, update_attribute, update_column(s), touch,
|
|
102
|
+
# toggle!, increment!, decrement! (through update_counters), destroy,
|
|
103
|
+
# destroy!, delete, becomes + save, Class.update(id), Class.destroy(id),
|
|
104
|
+
# Class.update_counters and relation.update_counters, Class.upsert and Class.upsert_all,
|
|
105
|
+
# relation.destroy_all. Still reachable, on purpose: relation.update_all
|
|
106
|
+
# (the erasure service clears label snapshots of an erased person — the
|
|
107
|
+
# one sanctioned mutation, recorded itself), relation.delete_all and
|
|
108
|
+
# Class.delete(id) (the purge service, past the retention period). Protect
|
|
109
|
+
# the table at the database level too (REVOKE UPDATE, DELETE).
|
|
110
|
+
def readonly?
|
|
111
|
+
persisted?
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# `delete` skips the read-only check Rails makes elsewhere.
|
|
115
|
+
def delete
|
|
116
|
+
raise ActiveRecord::ReadOnlyRecord, "#{self.class} is marked as readonly" if persisted?
|
|
117
|
+
|
|
118
|
+
super
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# upsert / upsert_all would rewrite an existing row through its uuid, and
|
|
122
|
+
# update_counters is how increment! / decrement! write, past the
|
|
123
|
+
# instance (through the class up to Rails 8.1, through a relation —
|
|
124
|
+
# `unscoped.where(id:).update_counters` — on Rails main). Class methods
|
|
125
|
+
# on Rails 7.1; Relation methods the class delegates to from 7.2 on —
|
|
126
|
+
# refused in both places.
|
|
127
|
+
#
|
|
128
|
+
# @api private
|
|
129
|
+
module InsertOnly
|
|
130
|
+
def update_counters(*, **)
|
|
131
|
+
raise ActiveRecord::ReadOnlyRecord, 'PermissionForge::AuditEvent is insert-only: counters cannot be updated'
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def upsert_all(*, **)
|
|
135
|
+
raise ActiveRecord::ReadOnlyRecord, 'PermissionForge::AuditEvent is insert-only: rows cannot be upserted'
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def upsert(*, **)
|
|
139
|
+
raise ActiveRecord::ReadOnlyRecord, 'PermissionForge::AuditEvent is insert-only: rows cannot be upserted'
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
singleton_class.prepend(InsertOnly)
|
|
143
|
+
relation_delegate_class(ActiveRecord::Relation).prepend(InsertOnly)
|
|
144
|
+
|
|
145
|
+
private
|
|
146
|
+
|
|
147
|
+
# No uniqueness validation (a query per insert): the unique index holds.
|
|
148
|
+
def assign_uuid
|
|
149
|
+
self.uuid = PermissionForge.configuration.uuid_generator.call if uuid.blank?
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def follow_subject_tenant
|
|
153
|
+
column = PermissionForge.configuration.resolved_tenant_column
|
|
154
|
+
return if subject_tenant_id.nil? || column.nil? || !has_attribute?(column)
|
|
155
|
+
|
|
156
|
+
write_attribute(column, subject_tenant_id)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|