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,220 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Api
|
|
5
|
+
class BaseController < PermissionForge.configuration.api_base_controller_class.constantize
|
|
6
|
+
include PermissionForge::ApiAuthentication
|
|
7
|
+
include PermissionForge::TenantRequired
|
|
8
|
+
include PermissionForge::ParamShapes
|
|
9
|
+
include PermissionForge::Api::ResponseRenderer
|
|
10
|
+
include PermissionForge::Api::Pagination
|
|
11
|
+
include PermissionForge::Api::SerializerResolver
|
|
12
|
+
|
|
13
|
+
# API-safe CSRF posture. A mutating request without a valid CSRF token
|
|
14
|
+
# gets an empty session and an empty cookie jar (`:null_session`), so a
|
|
15
|
+
# cross-site form post from a signed-in admin's browser reaches api_auth
|
|
16
|
+
# (and any host `current_user`) with no session: a session- or
|
|
17
|
+
# cookie-based api_auth refuses it. Clients that authenticate with a
|
|
18
|
+
# header (a token, an API key) are unaffected. `prepend: true` puts the
|
|
19
|
+
# check first in the chain, before verify_api_enabled! /
|
|
20
|
+
# authenticate_api_request! (ApiAuthentication) and before the host
|
|
21
|
+
# base controller's own callbacks — registered after them it would
|
|
22
|
+
# only empty the session once api_auth had already let the request in.
|
|
23
|
+
# ActionController::API bases have no forgery protection (and no
|
|
24
|
+
# cookies), so this is a no-op there.
|
|
25
|
+
# Verified like the admin screens (ForgeryProtection).
|
|
26
|
+
include PermissionForge::ForgeryProtection
|
|
27
|
+
|
|
28
|
+
protect_from_forgery with: :null_session, prepend: true, **pf_forgery_strategy if respond_to?(:protect_from_forgery)
|
|
29
|
+
|
|
30
|
+
AUDIT_SOURCE = 'api'
|
|
31
|
+
# Largest id a bigint column holds; past it PostgreSQL raises instead of matching nothing.
|
|
32
|
+
MAX_RECORD_ID = (2**63) - 1
|
|
33
|
+
|
|
34
|
+
before_action :set_api_locale
|
|
35
|
+
before_action :pf_require_tenant
|
|
36
|
+
# After api_auth (a refused request records its own event).
|
|
37
|
+
around_action :pf_audit_request
|
|
38
|
+
|
|
39
|
+
rescue_from ActiveRecord::RecordNotFound do
|
|
40
|
+
render json: { error: { status: 404, messages: I18n.t('permission_forge.api.errors.not_found') } }, status: :not_found
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
rescue_from ActiveRecord::RecordInvalid do |e|
|
|
44
|
+
render json: { error: { status: 422, messages: e.record.errors.messages } }, status: :unprocessable_entity
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
rescue_from ActionController::ParameterMissing, ActionController::BadRequest do |e|
|
|
48
|
+
render json: { error: { status: 422, messages: e.message } }, status: :unprocessable_entity
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
# Who acts (Audit::ActorResolver: audit_actor_resolver, the signed-in
|
|
54
|
+
# user, else an anonymous API client), request id, IP, the `reason`
|
|
55
|
+
# body key and one correlation id per mutating request.
|
|
56
|
+
def pf_audit_request(&)
|
|
57
|
+
PermissionForge::Audit::RequestContext.wrap(self, source: AUDIT_SOURCE, &)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Records a refused request so that no rollback takes it along.
|
|
61
|
+
def pf_audit_refused(event, **payload)
|
|
62
|
+
PermissionForge::Audit::RequestContext.refused(self, event, source: AUDIT_SOURCE, **payload)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def set_api_locale
|
|
66
|
+
locale = params[:locale] || extract_locale_from_accept_language
|
|
67
|
+
# Always assign a locale (default when none/invalid) so a locale set by
|
|
68
|
+
# one request can never leak into the next on a reused thread.
|
|
69
|
+
I18n.locale = if locale.present? && api_available_locale?(locale)
|
|
70
|
+
locale
|
|
71
|
+
else
|
|
72
|
+
I18n.default_locale
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def extract_locale_from_accept_language
|
|
77
|
+
header = request.env['HTTP_ACCEPT_LANGUAGE']
|
|
78
|
+
return nil if header.blank?
|
|
79
|
+
|
|
80
|
+
# Split on commas, drop q-weights, take the primary subtag of each tag.
|
|
81
|
+
# Matches bare tags ("en") as well as region/quality forms ("en-US;q=0.8").
|
|
82
|
+
header.split(',').filter_map do |part|
|
|
83
|
+
tag = part.split(';').first.to_s.strip
|
|
84
|
+
primary = tag.split('-').first&.downcase
|
|
85
|
+
primary if primary && api_available_locale?(primary)
|
|
86
|
+
end.first
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def api_available_locale?(locale)
|
|
90
|
+
allowed = PermissionForge.configuration.available_locales || PermissionForge::BUNDLED_LOCALES
|
|
91
|
+
Array(allowed).map(&:to_s).include?(locale.to_s)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# The record `params[param_key]` names in `scope` — the one lookup every
|
|
95
|
+
# API endpoint uses. Under api_record_identifier = :id only digits name a
|
|
96
|
+
# record: the database would cast '1abc', or a uuid that starts with
|
|
97
|
+
# digits, to 1 and answer for another record. A NUL byte names nothing
|
|
98
|
+
# (PostgreSQL raises on it). Raises RecordNotFound, rendered as a 404.
|
|
99
|
+
def find_record(scope, param_key = :identifier)
|
|
100
|
+
identifier = PermissionForge.configuration.api_record_identifier
|
|
101
|
+
value = params[param_key].to_s
|
|
102
|
+
raise ActiveRecord::RecordNotFound unless identifier_value?(identifier, value)
|
|
103
|
+
|
|
104
|
+
scope.find_by!(identifier => value)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def identifier_value?(identifier, value)
|
|
108
|
+
return false if value.empty? || value.include?("\0")
|
|
109
|
+
return true unless identifier.to_s == 'id'
|
|
110
|
+
|
|
111
|
+
value.match?(/\A[1-9]\d{0,18}\z/) && value.to_i <= MAX_RECORD_ID
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Looks up a host user by id within config.user_list_resolver
|
|
115
|
+
# (Admin::PeopleDirectory#find), rendering a 404 (and returning nil)
|
|
116
|
+
# when the id names nobody there — a missing user, someone the
|
|
117
|
+
# resolver leaves out, or a value that is not a key.
|
|
118
|
+
def find_host_user(user_id)
|
|
119
|
+
user = PermissionForge::Admin::PeopleDirectory.new.find(user_id)
|
|
120
|
+
render_not_found unless user
|
|
121
|
+
user
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def authorize_action!(action, record)
|
|
125
|
+
authorizer = PermissionForge.configuration.api_action_authorization
|
|
126
|
+
message = I18n.t('permission_forge.api.errors.action_not_authorized')
|
|
127
|
+
unless authorizer
|
|
128
|
+
return deny_when_authorization_required(message) do
|
|
129
|
+
audit_action_refused(action, record, hook: 'api_require_authorization')
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
return if authorizer.call(self, action, record)
|
|
134
|
+
|
|
135
|
+
audit_action_refused(action, record, hook: 'api_action_authorization')
|
|
136
|
+
render_not_authorized(message)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Runs the mutation authorization hook and the is_editable lock for a
|
|
140
|
+
# create/update/destroy. May render a 403; callers follow with
|
|
141
|
+
# `return if performed?`. Used by CrudActions and the bespoke controllers
|
|
142
|
+
# so every mutation path is guarded consistently.
|
|
143
|
+
# A destroy also asks the record's container (EditableLock.container_of):
|
|
144
|
+
# a set in a locked group is not deleted.
|
|
145
|
+
def guard_mutation!(action, record)
|
|
146
|
+
authorize_action!(action, record)
|
|
147
|
+
return if performed?
|
|
148
|
+
|
|
149
|
+
enforce_editable!(record, (PermissionForge::Admin::EditableLock.container_of(record) if action == :destroy))
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Blocks a change when a record, or what it belongs to (its Resource,
|
|
153
|
+
# Permission Set, or the Group of a membership — Admin::EditableLock),
|
|
154
|
+
# is flagged is_editable: false: a new child of a locked parent is
|
|
155
|
+
# refused like a change to the parent. Records with a nil/true flag are
|
|
156
|
+
# unaffected. The refusal names the locked record.
|
|
157
|
+
def enforce_editable!(*records)
|
|
158
|
+
reject_locked!(PermissionForge::Admin::EditableLock.locked_record(*records))
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def reject_locked!(locked)
|
|
162
|
+
return unless locked
|
|
163
|
+
|
|
164
|
+
pf_audit_refused('admin.lock_refused', record: locked)
|
|
165
|
+
render json: {
|
|
166
|
+
error: { status: 403, messages: I18n.t('permission_forge.api.errors.not_editable') }
|
|
167
|
+
}, status: :forbidden
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# config.api_assignment_authorization receives (controller, target,
|
|
171
|
+
# user_ids): the Permission Set (or the Group of a membership) and an
|
|
172
|
+
# Array of the people concerned cast to the user model's primary-key
|
|
173
|
+
# type (Integers for integer keys) — whatever shape the request sent.
|
|
174
|
+
# A refusal records the ids as requested (bounded, Audit::Refusals).
|
|
175
|
+
def authorize_assignment!(permission_set, user_ids)
|
|
176
|
+
authorizer = PermissionForge.configuration.api_assignment_authorization
|
|
177
|
+
message = I18n.t('permission_forge.api.errors.assignment_not_authorized')
|
|
178
|
+
unless authorizer
|
|
179
|
+
return deny_when_authorization_required(message) do
|
|
180
|
+
audit_assignment_refused(permission_set, user_ids, hook: 'api_require_authorization')
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
return if authorizer.call(self, permission_set, PermissionForge::Admin::PeopleDirectory::Ids.cast(user_ids))
|
|
185
|
+
|
|
186
|
+
audit_assignment_refused(permission_set, user_ids, hook: 'api_assignment_authorization')
|
|
187
|
+
render_not_authorized(message)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# When config.api_require_authorization is on, a missing authorization lambda
|
|
191
|
+
# means DENY (403) rather than the backward-compatible silent allow. This
|
|
192
|
+
# prevents "authenticated == full admin" when the host wired api_auth but not
|
|
193
|
+
# the per-action / assignment authorization hooks. The block records the
|
|
194
|
+
# refusal.
|
|
195
|
+
def deny_when_authorization_required(message)
|
|
196
|
+
return unless PermissionForge.configuration.api_require_authorization
|
|
197
|
+
|
|
198
|
+
yield
|
|
199
|
+
render_not_authorized(message)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# api.auth_failed for a mutation the authorization refused. A record
|
|
203
|
+
# not saved yet (create) is named by its class only.
|
|
204
|
+
def audit_action_refused(action, record, hook:)
|
|
205
|
+
reference = if record.respond_to?(:persisted?) && record.persisted? then { record: record }
|
|
206
|
+
elsif record then { record_type: record.class.name }
|
|
207
|
+
else {}
|
|
208
|
+
end
|
|
209
|
+
pf_audit_refused('api.auth_failed', data: { 'reason' => 'action_not_authorized', 'hook' => hook, 'action' => action.to_s },
|
|
210
|
+
**reference)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# `target` is the Permission Set, or the Permission Set Group of a
|
|
214
|
+
# membership; the user ids are bounded (Audit::Refusals).
|
|
215
|
+
def audit_assignment_refused(target, user_ids, hook:)
|
|
216
|
+
pf_audit_refused('api.auth_failed', **PermissionForge::Audit::Refusals.assignment(target, user_ids, hook: hook))
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Api
|
|
5
|
+
class DashboardController < BaseController
|
|
6
|
+
before_action :require_admin!, only: :show
|
|
7
|
+
|
|
8
|
+
def show
|
|
9
|
+
render json: {
|
|
10
|
+
data: {
|
|
11
|
+
permission_sets_count: PermissionForge::PermissionSet.count,
|
|
12
|
+
active_sets_count: PermissionForge::PermissionSet.active.count,
|
|
13
|
+
resources_count: PermissionForge::Resource.count,
|
|
14
|
+
rules_count: PermissionForge::PermissionSetRule.count,
|
|
15
|
+
groups_count: PermissionForge::PermissionSetGroup.count,
|
|
16
|
+
users_count: PermissionForge::PermissionSetUser.select(:user_id).distinct.count
|
|
17
|
+
}
|
|
18
|
+
}, status: :ok
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def require_admin!
|
|
24
|
+
return unless PermissionForge.configuration.api_stats_require_admin
|
|
25
|
+
|
|
26
|
+
head :forbidden unless @admin
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Api
|
|
5
|
+
# Members of one Permission Set Group. Adding and removing run through
|
|
6
|
+
# the model callbacks (group_membership.* audit events), after
|
|
7
|
+
# api_assignment_authorization and then config.admin_assignment_authorization.
|
|
8
|
+
class PermissionSetGroupUsersController < BaseController
|
|
9
|
+
include PermissionForge::AssignmentAuthorization
|
|
10
|
+
|
|
11
|
+
before_action :set_group
|
|
12
|
+
|
|
13
|
+
def index
|
|
14
|
+
scope = @group.permission_set_group_users.includes(:user)
|
|
15
|
+
render_collection(scope, serializer_for(:permission_set_group_user))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def create
|
|
19
|
+
user_id = params[:user_id]
|
|
20
|
+
authorize_assignment!(@group, [user_id])
|
|
21
|
+
enforce_editable!(@group) unless performed?
|
|
22
|
+
return if performed?
|
|
23
|
+
|
|
24
|
+
user = find_host_user(user_id)
|
|
25
|
+
return if performed? || pf_assignment_refused_api?(@group, user.id, :add_member)
|
|
26
|
+
|
|
27
|
+
gu = @group.permission_set_group_users.build(user: user)
|
|
28
|
+
save_and_render(gu, :permission_set_group_user)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The member must be somebody config.user_list_resolver lists (404 otherwise).
|
|
32
|
+
def destroy
|
|
33
|
+
gu = find_record(@group.permission_set_group_users)
|
|
34
|
+
return unless find_host_user(gu.user_id)
|
|
35
|
+
|
|
36
|
+
authorize_assignment!(@group, [gu.user_id])
|
|
37
|
+
enforce_editable!(gu) unless performed?
|
|
38
|
+
return if performed? || pf_assignment_refused_api?(@group, gu.user_id, :remove_member)
|
|
39
|
+
|
|
40
|
+
gu.destroy!
|
|
41
|
+
head :no_content
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def set_group
|
|
47
|
+
@group = find_record(PermissionForge::PermissionSetGroup, :permission_set_group_identifier)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Api
|
|
5
|
+
class PermissionSetGroupsController < BaseController
|
|
6
|
+
before_action :set_group, only: %i[show update destroy clone]
|
|
7
|
+
|
|
8
|
+
def index
|
|
9
|
+
scope = PermissionForge::PermissionSetGroup.order(:name)
|
|
10
|
+
render_collection(scope, serializer_for(:permission_set_group))
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def show
|
|
14
|
+
render_record(@group, serializer_for(:permission_set_group))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def create
|
|
18
|
+
group = PermissionForge::PermissionSetGroup.new(group_params)
|
|
19
|
+
guard_mutation!(:create, group)
|
|
20
|
+
return if performed?
|
|
21
|
+
|
|
22
|
+
if group.save
|
|
23
|
+
render_created(group, serializer_for(:permission_set_group))
|
|
24
|
+
else
|
|
25
|
+
render_validation_errors(group)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def update
|
|
30
|
+
guard_mutation!(:update, @group)
|
|
31
|
+
return if performed?
|
|
32
|
+
|
|
33
|
+
if @group.update(group_params)
|
|
34
|
+
render_record(@group, serializer_for(:permission_set_group))
|
|
35
|
+
else
|
|
36
|
+
render_validation_errors(@group)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def destroy
|
|
41
|
+
guard_mutation!(:destroy, @group)
|
|
42
|
+
return if performed?
|
|
43
|
+
|
|
44
|
+
@group.destroy!
|
|
45
|
+
head :no_content
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def clone
|
|
49
|
+
authorize_action!(:clone, @group)
|
|
50
|
+
return if performed?
|
|
51
|
+
|
|
52
|
+
# As the admin's Duplicate: the copy of the group and of its sets is editable.
|
|
53
|
+
cloned = PermissionForge::Services::PermissionSetGroupCloner.call(
|
|
54
|
+
permission_set_group: @group, attrs: { name: pf_text_param(:name).presence || "#{@group.name} (Copy)", is_editable: true },
|
|
55
|
+
unlock: true
|
|
56
|
+
)
|
|
57
|
+
render_created(cloned, serializer_for(:permission_set_group))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def set_group
|
|
63
|
+
@group = find_record(PermissionForge::PermissionSetGroup)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def group_params
|
|
67
|
+
pf_require_hash(:permission_set_group).permit(:name, :description, :is_active, :is_editable)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Api
|
|
5
|
+
class PermissionSetRulesController < BaseController
|
|
6
|
+
include CrudActions
|
|
7
|
+
include NestedResourceLookup
|
|
8
|
+
|
|
9
|
+
before_action :set_resource
|
|
10
|
+
before_action :set_rule, only: %i[show update destroy]
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def collection_scope = @resource.permission_set_rules
|
|
15
|
+
def current_record = @rule
|
|
16
|
+
def serializer_type = :permission_set_rule
|
|
17
|
+
|
|
18
|
+
def build_record
|
|
19
|
+
record = collection_scope.build(record_params)
|
|
20
|
+
record.permission_set = @permission_set
|
|
21
|
+
record
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def set_rule
|
|
25
|
+
@rule = find_nested_record(@resource.permission_set_rules)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def record_params
|
|
29
|
+
pf_require_hash(:permission_set_rule).permit(
|
|
30
|
+
:name, :field_name, :field_value, :operator,
|
|
31
|
+
:join_type, :field_accessor_path, :include_nulls,
|
|
32
|
+
:is_active, :is_editable, :failure_message
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Api
|
|
5
|
+
# Assignments of one Permission Set. Every grant and revocation runs
|
|
6
|
+
# through the model callbacks (assignment.* audit events), after
|
|
7
|
+
# api_assignment_authorization and then config.admin_assignment_authorization.
|
|
8
|
+
class PermissionSetUsersController < BaseController
|
|
9
|
+
include PermissionForge::AssignmentAuthorization
|
|
10
|
+
|
|
11
|
+
# bulk_action → AssignmentAuthorization action; the keys are the
|
|
12
|
+
# service's VALID_ACTIONS, anything else is a 422 before the service runs.
|
|
13
|
+
BULK_ACTIONS = { 'assign' => :grant, 'revoke' => :revoke }.freeze
|
|
14
|
+
|
|
15
|
+
before_action :set_permission_set
|
|
16
|
+
|
|
17
|
+
def index
|
|
18
|
+
scope = @permission_set.permission_set_users.includes(:user)
|
|
19
|
+
render_collection(scope, serializer_for(:permission_set_user))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def create
|
|
23
|
+
authorize_assignment!(@permission_set, [params[:user_id]])
|
|
24
|
+
reject_widening!(:grant) unless performed?
|
|
25
|
+
return if performed?
|
|
26
|
+
|
|
27
|
+
user = find_host_user(params[:user_id])
|
|
28
|
+
return if performed? || pf_assignment_refused_api?(@permission_set, user.id, :grant)
|
|
29
|
+
|
|
30
|
+
create_assignment(user)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# The holder must be somebody config.user_list_resolver lists (404
|
|
34
|
+
# otherwise). Revoking narrows access for a granting set, also a locked
|
|
35
|
+
# one; on a locked Muting Permission Set it widens and is refused.
|
|
36
|
+
def destroy
|
|
37
|
+
psu = find_record(@permission_set.permission_set_users)
|
|
38
|
+
return unless find_host_user(psu.user_id)
|
|
39
|
+
|
|
40
|
+
authorize_assignment!(@permission_set, [psu.user_id])
|
|
41
|
+
reject_widening!(:revoke) unless performed?
|
|
42
|
+
return if performed? || pf_assignment_refused_api?(@permission_set, psu.user_id, :revoke)
|
|
43
|
+
|
|
44
|
+
psu.destroy!
|
|
45
|
+
head :no_content
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def bulk
|
|
49
|
+
authorize_assignment!(@permission_set, params[:user_ids])
|
|
50
|
+
# On a locked set only what narrows access passes (an unknown
|
|
51
|
+
# bulk_action is judged as a grant, then refused as a 422).
|
|
52
|
+
reject_widening!(BULK_ACTIONS.fetch(params[:bulk_action].to_s, :grant)) unless performed?
|
|
53
|
+
return if performed? || bulk_refused?
|
|
54
|
+
|
|
55
|
+
render json: { data: bulk_assignment }, status: :ok
|
|
56
|
+
rescue ArgumentError => e
|
|
57
|
+
render json: { error: { status: 422, messages: e.message } }, status: :unprocessable_entity
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
# 403 when the change would widen access on a locked set
|
|
63
|
+
# (Admin::EditableLock.widening_lock: which way depends on is_muting).
|
|
64
|
+
def reject_widening!(change)
|
|
65
|
+
reject_locked!(pf_widening_lock(@permission_set, change))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def create_assignment(user)
|
|
69
|
+
psu = @permission_set.permission_set_users.build(user: user, expires_at: pf_time_param(:expires_at))
|
|
70
|
+
psu.save ? render_created(psu, serializer_for(:permission_set_user)) : render_validation_errors(psu)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# An unknown bulk_action raises ArgumentError (422, rescued in #bulk)
|
|
74
|
+
# before anything is authorized or changed.
|
|
75
|
+
def bulk_refused?
|
|
76
|
+
action = BULK_ACTIONS.fetch(params[:bulk_action].to_s) do |name|
|
|
77
|
+
PermissionForge::Services::BulkPermissionSetAssignment.validate_action!(name)
|
|
78
|
+
raise ArgumentError, "Invalid action: #{name}" # a service action this controller does not map
|
|
79
|
+
end
|
|
80
|
+
pf_assignment_refused_api?(@permission_set, params[:user_ids], action)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def bulk_assignment
|
|
84
|
+
PermissionForge::Services::BulkPermissionSetAssignment.call(
|
|
85
|
+
permission_set: @permission_set,
|
|
86
|
+
user_ids: bulk_user_ids,
|
|
87
|
+
action: params[:bulk_action],
|
|
88
|
+
created_by: PermissionForge.current_user,
|
|
89
|
+
expires_at: pf_time_param(:expires_at)
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# The people of the request within config.user_list_resolver
|
|
94
|
+
# (Admin::PeopleDirectory#find_all); an id naming nobody there is
|
|
95
|
+
# skipped, as an unknown id always was.
|
|
96
|
+
def bulk_user_ids
|
|
97
|
+
PermissionForge::Admin::PeopleDirectory.new.find_all(params[:user_ids]).map(&:id)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def set_permission_set
|
|
101
|
+
@permission_set = find_record(PermissionForge::PermissionSet, :permission_set_identifier)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Api
|
|
5
|
+
class PermissionSetsController < BaseController
|
|
6
|
+
before_action :set_permission_set, only: %i[show update destroy clone]
|
|
7
|
+
|
|
8
|
+
def index
|
|
9
|
+
scope = PermissionForge::PermissionSet.order(created_at: :desc)
|
|
10
|
+
render_collection(scope, serializer_for(:permission_set))
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def show
|
|
14
|
+
render_record(@permission_set, serializer_for(:permission_set))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def create
|
|
18
|
+
ps = PermissionForge::PermissionSet.new(permission_set_params)
|
|
19
|
+
guard_mutation!(:create, ps)
|
|
20
|
+
return if performed?
|
|
21
|
+
|
|
22
|
+
save_set(ps) { render_created(ps, serializer_for(:permission_set)) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# The lock is checked on the stored set, before the request's
|
|
26
|
+
# attributes (is_editable among them) are assigned.
|
|
27
|
+
def update
|
|
28
|
+
guard_mutation!(:update, @permission_set)
|
|
29
|
+
return if performed?
|
|
30
|
+
|
|
31
|
+
@permission_set.assign_attributes(permission_set_params)
|
|
32
|
+
save_set(@permission_set) { render_record(@permission_set, serializer_for(:permission_set)) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def destroy
|
|
36
|
+
guard_mutation!(:destroy, @permission_set)
|
|
37
|
+
return if performed?
|
|
38
|
+
|
|
39
|
+
@permission_set.destroy!
|
|
40
|
+
head :no_content
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def clone
|
|
44
|
+
authorize_action!(:clone, @permission_set)
|
|
45
|
+
return if performed?
|
|
46
|
+
|
|
47
|
+
# As the admin's Duplicate: an editable copy, outside a locked group.
|
|
48
|
+
cloned = PermissionForge::Admin::PermissionSetFactory.duplicate(
|
|
49
|
+
@permission_set, name: pf_text_param(:name).presence || "#{@permission_set.name} (Copy)"
|
|
50
|
+
)
|
|
51
|
+
render_created(cloned, serializer_for(:permission_set))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# Saves a set whose attributes are assigned, unless it would enter or
|
|
57
|
+
# leave a locked Permission Set Group (as the admin's group page
|
|
58
|
+
# refuses); yields to render on success.
|
|
59
|
+
def save_set(permission_set)
|
|
60
|
+
locked = locked_group_for_move(permission_set)
|
|
61
|
+
return reject_locked!(locked) if locked
|
|
62
|
+
|
|
63
|
+
permission_set.save ? yield : render_validation_errors(permission_set)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def locked_group_for_move(permission_set)
|
|
67
|
+
return unless permission_set.permission_set_group_id_changed?
|
|
68
|
+
|
|
69
|
+
PermissionForge::Admin::EditableLock.locked_group_for_move(
|
|
70
|
+
from: permission_set.permission_set_group_id_was, to: permission_set.permission_set_group_id
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def set_permission_set
|
|
75
|
+
@permission_set = find_record(PermissionForge::PermissionSet)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def permission_set_params
|
|
79
|
+
pf_require_hash(:permission_set).permit(
|
|
80
|
+
:name, :description, :is_active, :is_editable, :is_muting,
|
|
81
|
+
:requires_session_flag, :allowed_ips, :allowed_hours_start, :allowed_hours_end,
|
|
82
|
+
:permission_set_group_id
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Api
|
|
5
|
+
class ResourceActionsController < BaseController
|
|
6
|
+
include CrudActions
|
|
7
|
+
include NestedResourceLookup
|
|
8
|
+
|
|
9
|
+
before_action :set_resource
|
|
10
|
+
before_action :set_action, only: %i[show update destroy]
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def collection_scope = @resource.resource_actions
|
|
15
|
+
def current_record = @action
|
|
16
|
+
def serializer_type = :resource_action
|
|
17
|
+
|
|
18
|
+
def set_action
|
|
19
|
+
@action = find_nested_record(@resource.resource_actions)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def record_params
|
|
23
|
+
pf_require_hash(:resource_action).permit(:name, :label, :description, :is_permitted, :is_active)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# A create or a rename must also name a valid action key. Checked after
|
|
27
|
+
# the base checks (authorization, editable lock), so a caller who may
|
|
28
|
+
# not change the record gets its 403 first.
|
|
29
|
+
def guard_mutation!(action, record)
|
|
30
|
+
super
|
|
31
|
+
reject_invalid_key!(record) unless performed? || action == :destroy
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Action keys follow ControllerActions::KEY_FORMAT: the admin form, the
|
|
35
|
+
# controller catalog and permission_forge_actions accept nothing else.
|
|
36
|
+
# A row named "Export" or "archive " is not the key of `export` or
|
|
37
|
+
# `archive`: it would read as a restriction in the admin while that
|
|
38
|
+
# controller action stays unmanaged. Checked here rather than in a model
|
|
39
|
+
# validation, so rows that already carry such a name can still be edited
|
|
40
|
+
# and cloned.
|
|
41
|
+
def reject_invalid_key!(record)
|
|
42
|
+
name = requested_name
|
|
43
|
+
return if name.nil? || name.match?(PermissionForge::ControllerActions::KEY_FORMAT)
|
|
44
|
+
|
|
45
|
+
record.errors.add(:name, I18n.t('permission_forge.errors.admin.action_format'))
|
|
46
|
+
render_validation_errors(record)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# The name a create or a rename asks for; nil when an update keeps the
|
|
50
|
+
# name the row has.
|
|
51
|
+
def requested_name
|
|
52
|
+
current = current_record&.name
|
|
53
|
+
name = record_params.fetch(:name, current).to_s
|
|
54
|
+
name unless name == current
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Api
|
|
5
|
+
class ResourceFieldsController < BaseController
|
|
6
|
+
include CrudActions
|
|
7
|
+
include NestedResourceLookup
|
|
8
|
+
|
|
9
|
+
before_action :set_resource
|
|
10
|
+
before_action :set_field, only: %i[show update destroy]
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def collection_scope = @resource.resource_fields
|
|
15
|
+
def current_record = @field
|
|
16
|
+
def serializer_type = :resource_field
|
|
17
|
+
|
|
18
|
+
def set_field
|
|
19
|
+
@field = find_nested_record(@resource.resource_fields)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def record_params
|
|
23
|
+
pf_require_hash(:resource_field).permit(
|
|
24
|
+
:name, :label, :description, :is_active, :is_editable,
|
|
25
|
+
:read_access, :edit_access, :mask_access, :mask_strategy
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|