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,100 @@
|
|
|
1
|
+
module PermissionForge
|
|
2
|
+
# A named group of resource, field, action and rule grants (or, with `is_muting`, of revocations)
|
|
3
|
+
# that is assigned to users.
|
|
4
|
+
#
|
|
5
|
+
# @api public
|
|
6
|
+
class PermissionSet < ActiveRecord::Base
|
|
7
|
+
# The table name, with the configured prefix (read on every call, so a prefix set at runtime applies).
|
|
8
|
+
def self.table_name = "#{PermissionForge.table_name_prefix}permission_sets"
|
|
9
|
+
|
|
10
|
+
include PermissionForge::Concerns::HasActiveScope
|
|
11
|
+
include PermissionForge::Concerns::HasUuid
|
|
12
|
+
include PermissionForge::Concerns::ValidatesColumnLengths
|
|
13
|
+
include PermissionForge::Concerns::HasAuditTrail
|
|
14
|
+
include PermissionForge::Concerns::HasTenantScoping
|
|
15
|
+
include PermissionForge::Concerns::HasPermissionSetUsers
|
|
16
|
+
include PermissionForge::Concerns::PermissionsRegenerator
|
|
17
|
+
include PermissionForge::Concerns::InvalidatesManagedActions
|
|
18
|
+
include PermissionForge::Concerns::RecordsAuditEvents
|
|
19
|
+
|
|
20
|
+
permission_set_user_lookup :self
|
|
21
|
+
|
|
22
|
+
belongs_to :permission_set_group, class_name: 'PermissionForge::PermissionSetGroup', optional: true
|
|
23
|
+
# Every collection removes through callbacks (an audit event each):
|
|
24
|
+
# `dependent: :destroy` on :users destroys the assignment rows, never a
|
|
25
|
+
# person; AuditedCollection covers `clear` / `delete_all`.
|
|
26
|
+
has_many :resources, class_name: 'PermissionForge::Resource', dependent: :destroy, extend: Concerns::AuditedCollection
|
|
27
|
+
has_many :permission_set_rules, class_name: 'PermissionForge::PermissionSetRule', dependent: :destroy,
|
|
28
|
+
extend: Concerns::AuditedCollection
|
|
29
|
+
has_many :permission_set_users, class_name: 'PermissionForge::PermissionSetUser', dependent: :destroy,
|
|
30
|
+
inverse_of: :permission_set, extend: Concerns::AuditedCollection
|
|
31
|
+
has_many :users, through: :permission_set_users, dependent: :destroy, extend: Concerns::AuditedCollection
|
|
32
|
+
|
|
33
|
+
# Checked for a new row and on a rename only: a row whose name was
|
|
34
|
+
# blank before 0.14 stays editable in its other attributes.
|
|
35
|
+
validates :name, presence: { message: ->(*) { I18n.t('permission_forge.validations.name_blank') } },
|
|
36
|
+
if: -> { new_record? || will_save_change_to_name? }
|
|
37
|
+
validates :name, length: { maximum: 255 }
|
|
38
|
+
# A group id must name a group (of this tenant): optional, but never dangling.
|
|
39
|
+
validates :permission_set_group,
|
|
40
|
+
presence: { message: ->(*) { I18n.t('permission_forge.validations.permission_set.group_missing') } },
|
|
41
|
+
if: -> { permission_set_group_id.present? && will_save_change_to_permission_set_group_id? }
|
|
42
|
+
validates :allowed_hours_start, numericality: { in: 0..23, allow_nil: true }
|
|
43
|
+
validates :allowed_hours_end, numericality: { in: 0..23, allow_nil: true }
|
|
44
|
+
|
|
45
|
+
scope :muting, -> { where(is_muting: true) }
|
|
46
|
+
scope :granting, -> { where(is_muting: false) }
|
|
47
|
+
scope :conditional, lambda {
|
|
48
|
+
where.not(requires_session_flag: [nil, ''])
|
|
49
|
+
.or(where.not(allowed_ips: [nil, '']))
|
|
50
|
+
.or(where.not(allowed_hours_start: nil))
|
|
51
|
+
}
|
|
52
|
+
scope :unconditional, lambda {
|
|
53
|
+
where(requires_session_flag: [nil, ''])
|
|
54
|
+
.where(allowed_ips: [nil, ''])
|
|
55
|
+
.where(allowed_hours_start: nil)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
apply_tenant_scoping! # from the configuration, as the class loads (HasTenantScoping)
|
|
59
|
+
|
|
60
|
+
def active?
|
|
61
|
+
is_active
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def inactive?
|
|
65
|
+
!is_active
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def muting?
|
|
69
|
+
is_muting
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def granting?
|
|
73
|
+
!is_muting
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def conditional?
|
|
77
|
+
requires_session_flag.present? || allowed_ips.present? || allowed_hours_start.present?
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def unconditional?
|
|
81
|
+
!conditional?
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Deep-clones this permission set with all resources, fields, and rules.
|
|
85
|
+
# Does NOT copy user assignments — the clone starts with no users.
|
|
86
|
+
def deep_clone(attrs = {})
|
|
87
|
+
PermissionForge::Services::PermissionSetCloner.call(permission_set: self, attrs: attrs)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
# InvalidatesManagedActions: a new set has no resources yet and the index
|
|
93
|
+
# holds no set names, so only switching a set on or off changes what is
|
|
94
|
+
# managed. (Its resources drop the index themselves when they are added
|
|
95
|
+
# or destroyed.)
|
|
96
|
+
def managed_actions_changed?
|
|
97
|
+
!previously_new_record? && saved_change_to_is_active?
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
module PermissionForge
|
|
2
|
+
# An optional grouping of Permission Sets that is assigned to people as one.
|
|
3
|
+
#
|
|
4
|
+
# @api public
|
|
5
|
+
class PermissionSetGroup < ActiveRecord::Base
|
|
6
|
+
# The table name, with the configured prefix (read on every call, so a prefix set at runtime applies).
|
|
7
|
+
def self.table_name = "#{PermissionForge.table_name_prefix}permission_set_groups"
|
|
8
|
+
|
|
9
|
+
include PermissionForge::Concerns::HasActiveScope
|
|
10
|
+
include PermissionForge::Concerns::HasUuid
|
|
11
|
+
include PermissionForge::Concerns::ValidatesColumnLengths
|
|
12
|
+
include PermissionForge::Concerns::HasAuditTrail
|
|
13
|
+
include PermissionForge::Concerns::HasTenantScoping
|
|
14
|
+
include PermissionForge::Concerns::PermissionsRegenerator
|
|
15
|
+
include PermissionForge::Concerns::RecordsAuditEvents
|
|
16
|
+
|
|
17
|
+
# Deleting a group detaches its sets (detach_permission_sets) — not
|
|
18
|
+
# `dependent: :nullify`, whose UPDATE runs no callbacks and so leaves no
|
|
19
|
+
# audit event. A set removed from the collection (`delete`, `ids=`, `=`,
|
|
20
|
+
# `clear`) is detached the same way by `before_remove`, so Rails' own
|
|
21
|
+
# nullify that follows finds nothing left to change. Never
|
|
22
|
+
# `dependent: :destroy` here: that would delete the sets.
|
|
23
|
+
has_many :permission_sets, class_name: 'PermissionForge::PermissionSet',
|
|
24
|
+
before_remove: :detach_permission_set, extend: Concerns::AuditedCollection
|
|
25
|
+
has_many :permission_set_group_users, class_name: 'PermissionForge::PermissionSetGroupUser', dependent: :destroy,
|
|
26
|
+
extend: Concerns::AuditedCollection
|
|
27
|
+
# Destroys the membership rows, never a person.
|
|
28
|
+
has_many :users, through: :permission_set_group_users, dependent: :destroy, extend: Concerns::AuditedCollection
|
|
29
|
+
|
|
30
|
+
# Checked for a new row and on a rename only: a row whose name was
|
|
31
|
+
# blank before 0.14 stays editable in its other attributes.
|
|
32
|
+
validates :name, presence: { message: ->(*) { I18n.t('permission_forge.validations.name_blank') } },
|
|
33
|
+
if: -> { new_record? || will_save_change_to_name? }
|
|
34
|
+
|
|
35
|
+
before_destroy :detach_permission_sets
|
|
36
|
+
|
|
37
|
+
apply_tenant_scoping! # from the configuration, as the class loads (HasTenantScoping)
|
|
38
|
+
|
|
39
|
+
# Deep-clones this group with all its permission sets and their resources/fields/rules.
|
|
40
|
+
# Does NOT copy user assignments — the clone starts with no users.
|
|
41
|
+
def deep_clone(attrs = {})
|
|
42
|
+
PermissionForge::Services::PermissionSetGroupCloner.call(permission_set_group: self, attrs: attrs)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
# before_remove of #permission_sets: an audited save (a
|
|
48
|
+
# `permission_set.updated` event) instead of Rails' UPDATE. The row is
|
|
49
|
+
# what counts, not the instance: one loaded before the set joined this
|
|
50
|
+
# group still has the old foreign key, so the stored row is detached.
|
|
51
|
+
def detach_permission_set(set)
|
|
52
|
+
return unless set.persisted?
|
|
53
|
+
|
|
54
|
+
set = in_record_tenant(PermissionForge::PermissionSet).find_by(id: set.id, permission_set_group_id: id) unless
|
|
55
|
+
set.permission_set_group_id == id
|
|
56
|
+
return unless set
|
|
57
|
+
|
|
58
|
+
set.permission_set_group_id = nil
|
|
59
|
+
set.save!(validate: false)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# One audited save per set (a `permission_set.updated` event each, in the
|
|
63
|
+
# destroy's transaction, so they share its correlation id). The sets are
|
|
64
|
+
# read in the group's own tenant. Validation is skipped: a set that an
|
|
65
|
+
# unrelated rule would now refuse (a legacy over-long name) must still let
|
|
66
|
+
# go of a group that is being deleted.
|
|
67
|
+
def detach_permission_sets
|
|
68
|
+
in_record_tenant(PermissionForge::PermissionSet).where(permission_set_group_id: id).find_each do |set|
|
|
69
|
+
set.permission_set_group_id = nil
|
|
70
|
+
set.save!(validate: false)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def users_on_permission_set
|
|
75
|
+
return [] unless is_active
|
|
76
|
+
|
|
77
|
+
ps_ids = in_record_tenant(PermissionForge::PermissionSet).where(permission_set_group_id: id, is_active: true).pluck(:id)
|
|
78
|
+
return [] if ps_ids.empty?
|
|
79
|
+
|
|
80
|
+
in_record_tenant(PermissionForge::PermissionSetUser).where(permission_set_id: ps_ids)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module PermissionForge
|
|
2
|
+
# The membership of a user in a Permission Set Group (unique per user and group).
|
|
3
|
+
#
|
|
4
|
+
# @api public
|
|
5
|
+
class PermissionSetGroupUser < ActiveRecord::Base
|
|
6
|
+
# The table name, with the configured prefix (read on every call, so a prefix set at runtime applies).
|
|
7
|
+
def self.table_name = "#{PermissionForge.table_name_prefix}permission_set_group_users"
|
|
8
|
+
|
|
9
|
+
include PermissionForge::Concerns::HasUuid
|
|
10
|
+
include PermissionForge::Concerns::ValidatesColumnLengths
|
|
11
|
+
include PermissionForge::Concerns::HasAuditTrail
|
|
12
|
+
include PermissionForge::Concerns::HasTenantScoping
|
|
13
|
+
include PermissionForge::Concerns::RecordsAuditEvents
|
|
14
|
+
|
|
15
|
+
# optional: is stated (see PermissionSetUser): the group is optional as
|
|
16
|
+
# declared since 0.13, the user is required.
|
|
17
|
+
belongs_to :permission_set_group, class_name: 'PermissionForge::PermissionSetGroup', optional: true
|
|
18
|
+
belongs_to :user, class_name: PermissionForge.configuration.user_class, optional: false
|
|
19
|
+
|
|
20
|
+
# Mirrors the DB unique index so a repeat assignment returns a 422 instead of
|
|
21
|
+
# a RecordNotUnique 500 (and prevents silent duplicate rows on schemas that
|
|
22
|
+
# predate the index).
|
|
23
|
+
validates :user_id, uniqueness: { scope: :permission_set_group_id }
|
|
24
|
+
|
|
25
|
+
# See PermissionSetUser: never store a truncated user key.
|
|
26
|
+
before_validation { PermissionForge::Services::HostKeyGuard.verify! }
|
|
27
|
+
|
|
28
|
+
# The reason of the change in progress (Audit::Context) is kept on the row.
|
|
29
|
+
before_save :keep_audit_reason
|
|
30
|
+
|
|
31
|
+
apply_tenant_scoping! # from the configuration, as the class loads (HasTenantScoping)
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def keep_audit_reason
|
|
36
|
+
reason = PermissionForge::Audit::Context[:reason]
|
|
37
|
+
return if reason.blank? || !has_attribute?(:reason)
|
|
38
|
+
return unless new_record? || changed?
|
|
39
|
+
|
|
40
|
+
self.reason = PermissionForge::Audit::ColumnValues.value(reason.to_s)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
module PermissionForge
|
|
2
|
+
# A row-level query rule of a Permission Set: one operator applied to one field of one resource.
|
|
3
|
+
#
|
|
4
|
+
# @api public
|
|
5
|
+
class PermissionSetRule < ActiveRecord::Base
|
|
6
|
+
# The table name, with the configured prefix (read on every call, so a prefix set at runtime applies).
|
|
7
|
+
def self.table_name = "#{PermissionForge.table_name_prefix}permission_set_rules"
|
|
8
|
+
|
|
9
|
+
include PermissionForge::Concerns::HasActiveScope
|
|
10
|
+
include PermissionForge::Operators::Constants
|
|
11
|
+
include PermissionForge::Concerns::HasUuid
|
|
12
|
+
include PermissionForge::Concerns::ValidatesColumnLengths
|
|
13
|
+
include PermissionForge::Concerns::HasAuditTrail
|
|
14
|
+
include PermissionForge::Concerns::HasTenantScoping
|
|
15
|
+
include PermissionForge::Concerns::HasPermissionSetUsers
|
|
16
|
+
include PermissionForge::Concerns::PermissionsRegenerator
|
|
17
|
+
include PermissionForge::Concerns::RecordsAuditEvents
|
|
18
|
+
|
|
19
|
+
permission_set_user_lookup :direct
|
|
20
|
+
|
|
21
|
+
# optional: as declared since 0.13 (the foreign keys hold); stated, so the
|
|
22
|
+
# host's belongs_to_required_by_default decides nothing here.
|
|
23
|
+
belongs_to :permission_set, class_name: 'PermissionForge::PermissionSet', optional: true
|
|
24
|
+
belongs_to :resource, class_name: 'PermissionForge::Resource', optional: true
|
|
25
|
+
scope :recently_deactivated, -> { where(is_active: false).where('updated_at > ?', 7.days.ago) }
|
|
26
|
+
|
|
27
|
+
# join_type values that mean "filter on the model's own table". Any other
|
|
28
|
+
# join_type is a join rule: auto_query_scoping skips it, so it only applies
|
|
29
|
+
# through find_by/find_by! and permission_scoped.
|
|
30
|
+
FILTER_JOIN_TYPES = [nil, '', 'filter'].freeze
|
|
31
|
+
scope :join_type_rules, -> { where.not(join_type: FILTER_JOIN_TYPES) }
|
|
32
|
+
|
|
33
|
+
# Whether a join_type means "filter on the model's own table": nothing,
|
|
34
|
+
# blank or 'filter'. The one definition — the applier, the default scope
|
|
35
|
+
# and the RLS generator all ask here, so a row written past the model's
|
|
36
|
+
# validation is read the same way by all three.
|
|
37
|
+
def self.filter_join_type?(join_type)
|
|
38
|
+
join_type.blank? || join_type.to_s == 'filter'
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def join_rule?
|
|
42
|
+
!self.class.filter_join_type?(join_type)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
validates :operator, inclusion: {
|
|
46
|
+
in: OPERATORS,
|
|
47
|
+
message: I18n.t('permission_forge.validations.permission_set_rule.invalid_operator',
|
|
48
|
+
default: 'is not a valid operator')
|
|
49
|
+
}
|
|
50
|
+
# An operator that takes no value (Operators::Validations.valueless?)
|
|
51
|
+
# stores an empty one: the column is NOT NULL, and the natural API payload
|
|
52
|
+
# for a null check has no field_value.
|
|
53
|
+
before_validation { self.field_value = '' if field_value.nil? && valueless_operator? }
|
|
54
|
+
|
|
55
|
+
validates :field_value, presence: {
|
|
56
|
+
message: I18n.t('permission_forge.validations.permission_set_rule.field_value_required',
|
|
57
|
+
default: 'is required for this operator')
|
|
58
|
+
}, unless: :valueless_operator?
|
|
59
|
+
validates :join_type, inclusion: {
|
|
60
|
+
in: JOIN_TYPES,
|
|
61
|
+
message: I18n.t('permission_forge.validations.permission_set_rule.invalid_join_type',
|
|
62
|
+
default: 'is not a valid join type')
|
|
63
|
+
}, allow_nil: true, allow_blank: false
|
|
64
|
+
validates :field_name, presence: true
|
|
65
|
+
validates :field_name, format: {
|
|
66
|
+
with: /\A[a-zA-Z_][a-zA-Z0-9_.]*\z/,
|
|
67
|
+
message: I18n.t('permission_forge.validations.permission_set_rule.invalid_field_name',
|
|
68
|
+
default: 'must be a valid column name (letters, numbers, underscores, dots)')
|
|
69
|
+
}, allow_blank: true
|
|
70
|
+
|
|
71
|
+
validate :validate_field_name_segments, if: :field_name_changed?
|
|
72
|
+
validate :validate_field_value_for_operator
|
|
73
|
+
|
|
74
|
+
apply_tenant_scoping! # from the configuration, as the class loads (HasTenantScoping)
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def valueless_operator?
|
|
79
|
+
PermissionForge::Operators::Validations.valueless?(operator)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# `flag.` or `a..b` passes the format above, but an empty segment makes it
|
|
83
|
+
# another name than the one written (see Operators::RuleColumn.name?).
|
|
84
|
+
# Checked for a new rule and on a rename only: a row that already carries
|
|
85
|
+
# such a name stays editable in its other attributes (it matches nothing).
|
|
86
|
+
def validate_field_name_segments
|
|
87
|
+
return if field_name.blank? || errors[:field_name].any?
|
|
88
|
+
return if PermissionForge::Operators::RuleColumn.name?(field_name)
|
|
89
|
+
|
|
90
|
+
errors.add(:field_name, I18n.t('permission_forge.validations.permission_set_rule.invalid_field_name',
|
|
91
|
+
default: 'must be a valid column name (letters, numbers, underscores, dots)'))
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def validate_field_value_for_operator
|
|
95
|
+
result = PermissionForge::Operators::Validations.validate(operator, field_value)
|
|
96
|
+
errors.add(result[:field], result[:message]) if result
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
module PermissionForge
|
|
2
|
+
# The assignment of a Permission Set to a user (unique per user and set), optionally with an `expires_at`.
|
|
3
|
+
#
|
|
4
|
+
# @api public
|
|
5
|
+
class PermissionSetUser < ActiveRecord::Base
|
|
6
|
+
# The table name, with the configured prefix (read on every call, so a prefix set at runtime applies).
|
|
7
|
+
def self.table_name = "#{PermissionForge.table_name_prefix}permission_set_users"
|
|
8
|
+
|
|
9
|
+
include PermissionForge::Concerns::HasUuid
|
|
10
|
+
include PermissionForge::Concerns::ValidatesColumnLengths
|
|
11
|
+
include PermissionForge::Concerns::HasAuditTrail
|
|
12
|
+
include PermissionForge::Concerns::HasTenantScoping
|
|
13
|
+
include PermissionForge::Concerns::InvalidatesInBatch
|
|
14
|
+
include PermissionForge::Concerns::RecordsAuditEvents
|
|
15
|
+
|
|
16
|
+
# optional: is stated on every belongs_to: the host's
|
|
17
|
+
# belongs_to_required_by_default decides nothing here. The parent is
|
|
18
|
+
# optional, as declared since 0.13 (the foreign key holds); the user is
|
|
19
|
+
# required.
|
|
20
|
+
belongs_to :permission_set, class_name: 'PermissionForge::PermissionSet', optional: true
|
|
21
|
+
belongs_to :user, class_name: PermissionForge.configuration.user_class, optional: false
|
|
22
|
+
|
|
23
|
+
# Mirrors the DB unique index so a repeat assignment returns a 422 instead of
|
|
24
|
+
# a RecordNotUnique 500 (and prevents silent duplicate rows on schemas that
|
|
25
|
+
# predate the index). BulkPermissionSetAssignment already de-dupes; this
|
|
26
|
+
# covers the single-assignment controller/API paths.
|
|
27
|
+
validates :user_id, uniqueness: { scope: :permission_set_id }
|
|
28
|
+
|
|
29
|
+
# An integer user_id column would store a UUID or string user key as its
|
|
30
|
+
# leading digits, handing this assignment to every user who shares them.
|
|
31
|
+
before_validation { PermissionForge::Services::HostKeyGuard.verify! }
|
|
32
|
+
|
|
33
|
+
# The reason of the change in progress (Audit::Context, e.g. a ticket
|
|
34
|
+
# from the request) is kept on the row it changed.
|
|
35
|
+
before_save :keep_audit_reason
|
|
36
|
+
|
|
37
|
+
# A move to another user takes the set from the previous holder: noted
|
|
38
|
+
# at every save, invalidated with the holder after the commit
|
|
39
|
+
# (InvalidatesInBatch#note_previous_value).
|
|
40
|
+
after_save { note_previous_value('user_id') }
|
|
41
|
+
|
|
42
|
+
scope :active_assignments, -> { where(expires_at: nil).or(where(expires_at: Time.current..)) }
|
|
43
|
+
scope :expired, -> { where.not(expires_at: nil).where(expires_at: ...Time.current) }
|
|
44
|
+
|
|
45
|
+
# Permanent assignments last reviewed before `cutoff` — the access
|
|
46
|
+
# review list and StaleAssignmentAuditor. Reviewed means a "Keep"
|
|
47
|
+
# (reviewed_at), else the grant itself (created_at); a schema without
|
|
48
|
+
# the review columns (upgrade migration not run) keeps measuring from
|
|
49
|
+
# updated_at, as before.
|
|
50
|
+
scope :stale_before, ->(cutoff) { where(expires_at: nil).where(last_reviewed_column.lt(cutoff)) }
|
|
51
|
+
|
|
52
|
+
apply_tenant_scoping! # from the configuration, as the class loads (HasTenantScoping)
|
|
53
|
+
|
|
54
|
+
# The review columns exist (bin/rails generate permission_forge:upgrade).
|
|
55
|
+
def self.review_supported?
|
|
56
|
+
column_names.include?('reviewed_at')
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Arel for "when this assignment was last reviewed" (see stale_before).
|
|
60
|
+
def self.last_reviewed_column
|
|
61
|
+
table = arel_table
|
|
62
|
+
return table[:updated_at] unless review_supported?
|
|
63
|
+
|
|
64
|
+
Arel::Nodes::NamedFunction.new('COALESCE', [table[:reviewed_at], table[:created_at]])
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# When this assignment was last reviewed (see stale_before).
|
|
68
|
+
def last_reviewed_at
|
|
69
|
+
return updated_at unless self.class.review_supported?
|
|
70
|
+
|
|
71
|
+
reviewed_at || created_at
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Access review "Keep": records who reviewed the assignment and when,
|
|
75
|
+
# through a normal update that writes one assignment.reviewed event
|
|
76
|
+
# (with the decision; the reason comes from Audit::Context). Without the
|
|
77
|
+
# review columns only updated_at moves, so the event is recorded itself.
|
|
78
|
+
#
|
|
79
|
+
# @param reviewer [Object, nil] the acting admin (its id is stored)
|
|
80
|
+
# @param decision [String]
|
|
81
|
+
# @return [self]
|
|
82
|
+
def mark_reviewed!(reviewer:, decision: 'keep')
|
|
83
|
+
data = { 'decision' => decision }
|
|
84
|
+
if self.class.review_supported?
|
|
85
|
+
attrs = { reviewed_at: Time.current }
|
|
86
|
+
attrs[:reviewed_by_id] = reviewer&.id if has_attribute?(:reviewed_by_id)
|
|
87
|
+
audit_as('assignment.reviewed', data: data) { update!(attrs) }
|
|
88
|
+
else
|
|
89
|
+
record_review_without_columns(data)
|
|
90
|
+
end
|
|
91
|
+
self
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def deactivated?
|
|
95
|
+
active_check = PermissionForge.configuration.user_active_check
|
|
96
|
+
!active_check.call(user)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def expired?
|
|
100
|
+
expires_at.present? && expires_at < Time.current
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def active_assignment?
|
|
104
|
+
!expired?
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
def record_review_without_columns(data)
|
|
110
|
+
transaction do
|
|
111
|
+
update!(updated_at: Time.current)
|
|
112
|
+
PermissionForge::Audit.record('assignment.reviewed', record: self, data: data)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def keep_audit_reason
|
|
117
|
+
reason = PermissionForge::Audit::Context[:reason]
|
|
118
|
+
return if reason.blank? || !has_attribute?(:reason)
|
|
119
|
+
return unless new_record? || changed?
|
|
120
|
+
|
|
121
|
+
self.reason = PermissionForge::Audit::ColumnValues.value(reason.to_s)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# InvalidatesInBatch (after commit, on create/update/destroy).
|
|
125
|
+
#
|
|
126
|
+
# Assigning or revoking a set must take effect immediately: invalidate the
|
|
127
|
+
# user's cached hash so the next read rebuilds it (read-through). This also
|
|
128
|
+
# covers PermissionSet#destroy — dependent: :destroy removes each assignment
|
|
129
|
+
# through callbacks, so every previously-assigned user is invalidated even
|
|
130
|
+
# though the set's own after_commit can no longer find the deleted rows.
|
|
131
|
+
def invalidate_committed_caches
|
|
132
|
+
super
|
|
133
|
+
invalidate_user_permissions
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def invalidate_user_permissions
|
|
137
|
+
holders = [user_id, *previous_values('user_id')]
|
|
138
|
+
invalidate_assigned_users(holders, invalidation_batch, permission_record_tenant_id)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# RecordsAuditEvents: the event of a move names the previous holder too
|
|
142
|
+
# (its subject is the new one; data["changes"]["user_id"] has both).
|
|
143
|
+
def audit_update_details
|
|
144
|
+
return {} unless saved_change_to_user_id?
|
|
145
|
+
|
|
146
|
+
{ 'previous_subject_user_id' => user_id_before_last_save&.to_s }
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# The holder of this assignment is at hand when the assignment was built
|
|
150
|
+
# with it (`create!(user: user)`): no query to load it again.
|
|
151
|
+
def assigned_users(user_ids)
|
|
152
|
+
holder = association(:user).target
|
|
153
|
+
return [holder] if holder && user_ids == [holder.id]
|
|
154
|
+
|
|
155
|
+
super
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
module PermissionForge
|
|
2
|
+
# Entity-level CRUD access to one model (its `name`) inside one Permission Set.
|
|
3
|
+
#
|
|
4
|
+
# @api public
|
|
5
|
+
class Resource < ActiveRecord::Base
|
|
6
|
+
# The table name, with the configured prefix (read on every call, so a prefix set at runtime applies).
|
|
7
|
+
def self.table_name = "#{PermissionForge.table_name_prefix}resources"
|
|
8
|
+
|
|
9
|
+
include PermissionForge::Concerns::HasActiveScope
|
|
10
|
+
include PermissionForge::Concerns::HasUuid
|
|
11
|
+
include PermissionForge::Concerns::ValidatesColumnLengths
|
|
12
|
+
include PermissionForge::Concerns::HasAuditTrail
|
|
13
|
+
include PermissionForge::Concerns::HasTenantScoping
|
|
14
|
+
include PermissionForge::Concerns::HasPermissionSetUsers
|
|
15
|
+
include PermissionForge::Concerns::ValidatesDependency
|
|
16
|
+
include PermissionForge::Concerns::PermissionsRegenerator
|
|
17
|
+
include PermissionForge::Concerns::InvalidatesManagedActions
|
|
18
|
+
include PermissionForge::Concerns::RecordsAuditEvents
|
|
19
|
+
|
|
20
|
+
# A Ruby constant path: Resource#name is resolved to a model class.
|
|
21
|
+
NAME_FORMAT = PermissionForge::RESOURCE_NAME_FORMAT
|
|
22
|
+
|
|
23
|
+
permission_set_user_lookup :direct
|
|
24
|
+
|
|
25
|
+
# Active rows of active Permission Sets — the rows that count right now.
|
|
26
|
+
scope :in_effect, -> { active.joins(:permission_set).merge(PermissionForge::PermissionSet.active) }
|
|
27
|
+
|
|
28
|
+
# A model named for the first time is masked in this process and every
|
|
29
|
+
# other one (MaskingSync). Noted per save, consumed once committed.
|
|
30
|
+
after_save :note_newly_named_model
|
|
31
|
+
after_commit :announce_newly_named_model
|
|
32
|
+
|
|
33
|
+
# optional: as declared since 0.13 (the foreign key holds); stated, so the
|
|
34
|
+
# host's belongs_to_required_by_default decides nothing here.
|
|
35
|
+
belongs_to :permission_set, class_name: 'PermissionForge::PermissionSet', optional: true
|
|
36
|
+
has_many :resource_fields, class_name: 'PermissionForge::ResourceField', dependent: :destroy,
|
|
37
|
+
extend: Concerns::AuditedCollection
|
|
38
|
+
has_many :resource_actions, class_name: 'PermissionForge::ResourceAction', dependent: :destroy,
|
|
39
|
+
extend: Concerns::AuditedCollection
|
|
40
|
+
has_many :permission_set_rules, class_name: 'PermissionForge::PermissionSetRule', dependent: :destroy,
|
|
41
|
+
extend: Concerns::AuditedCollection
|
|
42
|
+
|
|
43
|
+
validates :name, presence: true
|
|
44
|
+
validates :name, uniqueness: { scope: :permission_set_id, message: 'already exists in this permission set' }
|
|
45
|
+
# Resource#name is resolved to a model class via safe_constantize (e.g. by
|
|
46
|
+
# ResourceFieldGenerator and MaskedAttributes auto-apply), so constrain it to
|
|
47
|
+
# a Ruby constant path. Prevents naming a resource after arbitrary strings.
|
|
48
|
+
validates :name, format: {
|
|
49
|
+
with: NAME_FORMAT,
|
|
50
|
+
message: I18n.t('permission_forge.validations.resource.invalid_name',
|
|
51
|
+
default: 'must be a valid model class name (e.g. Customer or Admin::Report)')
|
|
52
|
+
}, allow_blank: true
|
|
53
|
+
|
|
54
|
+
validates_dependency :edit_access, requires: :read_access
|
|
55
|
+
validates_dependency :delete_access, requires: :read_access
|
|
56
|
+
validates_dependency :modify_all, requires: :view_all
|
|
57
|
+
|
|
58
|
+
apply_tenant_scoping! # from the configuration, as the class loads (HasTenantScoping)
|
|
59
|
+
|
|
60
|
+
# Fallback for installations that haven't added the label column yet.
|
|
61
|
+
def label
|
|
62
|
+
has_attribute?(:label) ? read_attribute(:label) : name
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# For a loaded record: active, in an active Permission Set (see .in_effect).
|
|
66
|
+
def in_effect?
|
|
67
|
+
is_active && permission_set.is_active
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# View All / Modify All skip every record rule on this resource.
|
|
71
|
+
def rules_bypassed?
|
|
72
|
+
view_all || modify_all
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Grants full CRUD access + all field access for a resource on a permission set.
|
|
76
|
+
# Intended for seeds, migrations, or console use only.
|
|
77
|
+
def self.grant_full_access!(permission_set:, resource_name:)
|
|
78
|
+
PermissionForge::Services::ResourceWithFieldsBuilder.call(
|
|
79
|
+
permission_set: permission_set, resource_name: resource_name
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def note_newly_named_model
|
|
86
|
+
@pf_newly_named = true if previously_new_record? || saved_change_to_name?
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# The concern is applied here whatever the other rows; the other
|
|
90
|
+
# processes are told when no other row (in any tenant) had the name, or
|
|
91
|
+
# this process had not masked the model yet (MaskingSync.named!, which
|
|
92
|
+
# asks for the other rows only when the answer decides something).
|
|
93
|
+
def announce_newly_named_model
|
|
94
|
+
return unless @pf_newly_named && persisted?
|
|
95
|
+
|
|
96
|
+
@pf_newly_named = false
|
|
97
|
+
PermissionForge::MaskingSync.named!(name) { self.class.unscoped.where(name: name).where.not(id: id).exists? }
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module PermissionForge
|
|
2
|
+
# A custom action (such as sync or export) that a Permission Set permits or withholds on a resource.
|
|
3
|
+
#
|
|
4
|
+
# @api public
|
|
5
|
+
class ResourceAction < ActiveRecord::Base
|
|
6
|
+
# The table name, with the configured prefix (read on every call, so a prefix set at runtime applies).
|
|
7
|
+
def self.table_name = "#{PermissionForge.table_name_prefix}resource_actions"
|
|
8
|
+
|
|
9
|
+
include PermissionForge::Concerns::HasActiveScope
|
|
10
|
+
include PermissionForge::Concerns::HasUuid
|
|
11
|
+
include PermissionForge::Concerns::ValidatesColumnLengths
|
|
12
|
+
include PermissionForge::Concerns::HasAuditTrail
|
|
13
|
+
include PermissionForge::Concerns::HasTenantScoping
|
|
14
|
+
include PermissionForge::Concerns::HasPermissionSetUsers
|
|
15
|
+
include PermissionForge::Concerns::PermissionsRegenerator
|
|
16
|
+
include PermissionForge::Concerns::InvalidatesManagedActions
|
|
17
|
+
include PermissionForge::Concerns::RecordsAuditEvents
|
|
18
|
+
|
|
19
|
+
permission_set_user_lookup :via_resource
|
|
20
|
+
|
|
21
|
+
# optional: as declared since 0.13 (the foreign key holds); stated, so the
|
|
22
|
+
# host's belongs_to_required_by_default decides nothing here.
|
|
23
|
+
belongs_to :resource, class_name: 'PermissionForge::Resource', optional: true
|
|
24
|
+
|
|
25
|
+
# One row per (resource, name): two rows would be OR-ed, so a second row
|
|
26
|
+
# could permit what the first one blocks. New installs also have a DB unique
|
|
27
|
+
# index (idx_pf_resource_actions_unique); the validation surfaces a 422
|
|
28
|
+
# instead of a RecordNotUnique 500. Checked when a row is created, renamed
|
|
29
|
+
# or moved, so duplicates that predate the index stay editable; the
|
|
30
|
+
# security audit lists them (`duplicate_actions`).
|
|
31
|
+
# Checked for a new row and on a rename only: a row whose name was
|
|
32
|
+
# blank before 0.14 stays editable in its other attributes.
|
|
33
|
+
validates :name, presence: { message: ->(*) { I18n.t('permission_forge.validations.name_blank') } },
|
|
34
|
+
if: -> { new_record? || will_save_change_to_name? }
|
|
35
|
+
validates :name, uniqueness: { scope: :resource_id,
|
|
36
|
+
message: ->(*) { I18n.t('permission_forge.errors.admin.action_taken') } },
|
|
37
|
+
if: -> { new_record? || will_save_change_to_name? || will_save_change_to_resource_id? }
|
|
38
|
+
|
|
39
|
+
# Active rows of active resources of active Permission Sets. An action with
|
|
40
|
+
# such a row is "managed": only people a set permits can run it.
|
|
41
|
+
scope :in_effect, lambda {
|
|
42
|
+
active.joins(resource: :permission_set)
|
|
43
|
+
.merge(PermissionForge::Resource.active).merge(PermissionForge::PermissionSet.active)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
apply_tenant_scoping! # from the configuration, as the class loads (HasTenantScoping)
|
|
47
|
+
|
|
48
|
+
# For a loaded record (see .in_effect).
|
|
49
|
+
def in_effect?
|
|
50
|
+
is_active && resource.in_effect?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @return [String] the label, or the humanized name when there is none
|
|
54
|
+
def display_name
|
|
55
|
+
label.presence || name.humanize
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|