permission_forge 0.14.1 → 0.15.0
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 +4 -4
- data/CHANGELOG.md +2372 -0
- data/README.md +76 -25
- data/app/controllers/concerns/permission_forge/api/crud_actions.rb +3 -2
- data/app/controllers/concerns/permission_forge/api/input_shapes.rb +200 -0
- data/app/controllers/concerns/permission_forge/api/pagination.rb +12 -5
- data/app/controllers/concerns/permission_forge/api/response_renderer.rb +37 -1
- data/app/controllers/concerns/permission_forge/api_authentication.rb +13 -2
- data/app/controllers/concerns/permission_forge/cache_confirmation.rb +42 -11
- data/app/controllers/concerns/permission_forge/change_review.rb +42 -0
- data/app/controllers/concerns/permission_forge/copy_refusal.rb +39 -0
- data/app/controllers/concerns/permission_forge/param_shapes.rb +207 -7
- data/app/controllers/concerns/permission_forge/save_notice.rb +69 -0
- data/app/controllers/permission_forge/api/audit_events_controller.rb +1 -1
- data/app/controllers/permission_forge/api/base_controller.rb +163 -12
- data/app/controllers/permission_forge/api/dashboard_controller.rb +29 -4
- data/app/controllers/permission_forge/api/permission_set_group_users_controller.rb +5 -2
- data/app/controllers/permission_forge/api/permission_set_groups_controller.rb +5 -4
- data/app/controllers/permission_forge/api/permission_set_rules_controller.rb +7 -5
- data/app/controllers/permission_forge/api/permission_set_users_controller.rb +80 -19
- data/app/controllers/permission_forge/api/permission_sets_controller.rb +12 -6
- data/app/controllers/permission_forge/api/resource_actions_controller.rb +1 -1
- data/app/controllers/permission_forge/api/resource_fields_controller.rb +3 -4
- data/app/controllers/permission_forge/api/resources_controller.rb +26 -12
- data/app/controllers/permission_forge/api/unmatched_routes_controller.rb +20 -0
- data/app/controllers/permission_forge/application_controller.rb +39 -5
- data/app/controllers/permission_forge/assignments_controller.rb +36 -34
- data/app/controllers/permission_forge/bulk_assignments_controller.rb +8 -4
- data/app/controllers/permission_forge/field_lookups_controller.rb +1 -1
- data/app/controllers/permission_forge/maintenance_controller.rb +6 -2
- data/app/controllers/permission_forge/people_controller.rb +14 -14
- data/app/controllers/permission_forge/permission_set_editor.rb +20 -6
- data/app/controllers/permission_forge/permission_set_group_users_controller.rb +3 -1
- data/app/controllers/permission_forge/permission_set_groups_controller.rb +23 -9
- data/app/controllers/permission_forge/permission_set_rules_controller.rb +20 -10
- data/app/controllers/permission_forge/permission_sets_controller.rb +23 -24
- data/app/controllers/permission_forge/resource_actions_controller.rb +11 -6
- data/app/controllers/permission_forge/resource_fields_controller.rb +37 -19
- data/app/controllers/permission_forge/resources_controller.rb +12 -11
- data/app/frontend/permission_forge/pf_admin.css +99 -17
- data/app/frontend/permission_forge/pf_admin.js +36 -0
- data/app/helpers/permission_forge/application_helper.rb +22 -72
- data/app/helpers/permission_forge/audit_attribution_helper.rb +77 -0
- data/app/helpers/permission_forge/audit_helper.rb +57 -119
- data/app/helpers/permission_forge/audit_records_helper.rb +103 -0
- data/app/helpers/permission_forge/audit_sentences_helper.rb +56 -2
- data/app/helpers/permission_forge/badges_helper.rb +74 -17
- data/app/helpers/permission_forge/changes_helper.rb +24 -6
- data/app/helpers/permission_forge/conditions_helper.rb +119 -0
- data/app/helpers/permission_forge/labels_helper.rb +134 -0
- data/app/helpers/permission_forge/people_helper.rb +11 -19
- data/app/helpers/permission_forge/rule_problems_helper.rb +73 -0
- data/app/helpers/permission_forge/sentences_helper.rb +119 -98
- data/app/helpers/permission_forge/settings_helper.rb +2 -2
- data/app/helpers/permission_forge/ui_helper.rb +65 -4
- data/app/models/permission_forge/audit_event.rb +5 -1
- data/app/models/permission_forge/permission_set.rb +208 -18
- data/app/models/permission_forge/permission_set_group.rb +10 -4
- data/app/models/permission_forge/permission_set_group_user.rb +3 -1
- data/app/models/permission_forge/permission_set_rule.rb +254 -17
- data/app/models/permission_forge/permission_set_user.rb +38 -3
- data/app/models/permission_forge/resource.rb +18 -8
- data/app/models/permission_forge/resource_action.rb +15 -6
- data/app/models/permission_forge/resource_field.rb +97 -11
- data/app/views/permission_forge/assignments/confirm_destroy.html.erb +4 -4
- data/app/views/permission_forge/assignments/new.html.erb +10 -6
- data/app/views/permission_forge/audit_events/index.html.erb +1 -1
- data/app/views/permission_forge/audit_events/show.html.erb +16 -9
- data/app/views/permission_forge/bulk_assignments/confirm_revoke.html.erb +3 -3
- data/app/views/permission_forge/bulk_assignments/new.html.erb +3 -3
- data/app/views/permission_forge/field_lookups/show.html.erb +2 -2
- data/app/views/permission_forge/home/_broken_rules_card.html.erb +35 -0
- data/app/views/permission_forge/home/_conditions_card.html.erb +28 -0
- data/app/views/permission_forge/home/_ending_card.html.erb +11 -5
- data/app/views/permission_forge/home/_expired_card.html.erb +1 -1
- data/app/views/permission_forge/home/_mask_strategies_card.html.erb +27 -0
- data/app/views/permission_forge/home/_privacy_card.html.erb +21 -7
- data/app/views/permission_forge/home/_rules_card.html.erb +1 -1
- data/app/views/permission_forge/home/_secret_rules_card.html.erb +33 -0
- data/app/views/permission_forge/home/_stale_card.html.erb +7 -5
- data/app/views/permission_forge/home/_tidy_card.html.erb +1 -1
- data/app/views/permission_forge/home/index.html.erb +5 -6
- data/app/views/permission_forge/people/_access_tab.html.erb +26 -19
- data/app/views/permission_forge/people/_audit_tab.html.erb +8 -8
- data/app/views/permission_forge/people/_effective_field_source.html.erb +13 -7
- data/app/views/permission_forge/people/_effective_tab.html.erb +16 -15
- data/app/views/permission_forge/people/_effective_why.html.erb +20 -16
- data/app/views/permission_forge/people/_why_item.html.erb +1 -1
- data/app/views/permission_forge/people/confirm_remove_all.html.erb +3 -3
- data/app/views/permission_forge/people/erasure_preview.html.erb +1 -1
- data/app/views/permission_forge/permission_set_groups/confirm_destroy.html.erb +3 -3
- data/app/views/permission_forge/permission_set_groups/index.html.erb +3 -3
- data/app/views/permission_forge/permission_set_groups/show.html.erb +28 -19
- data/app/views/permission_forge/permission_set_rules/_builder.html.erb +35 -15
- data/app/views/permission_forge/permission_set_rules/_preview.html.erb +17 -0
- data/app/views/permission_forge/permission_set_rules/confirm_destroy.html.erb +3 -3
- data/app/views/permission_forge/permission_set_rules/preview.html.erb +12 -0
- data/app/views/permission_forge/permission_sets/_assignments_tab.html.erb +37 -22
- data/app/views/permission_forge/permission_sets/_audit_tab.html.erb +4 -4
- data/app/views/permission_forge/permission_sets/_condition_problems.html.erb +15 -0
- data/app/views/permission_forge/permission_sets/_conditions_tab.html.erb +14 -8
- data/app/views/permission_forge/permission_sets/_header.html.erb +6 -5
- data/app/views/permission_forge/permission_sets/_records_summary.html.erb +2 -2
- data/app/views/permission_forge/permission_sets/_resources_tab.html.erb +4 -4
- data/app/views/permission_forge/permission_sets/confirm_destroy.html.erb +4 -4
- data/app/views/permission_forge/permission_sets/confirm_switch_mode.html.erb +3 -3
- data/app/views/permission_forge/permission_sets/index.html.erb +6 -6
- data/app/views/permission_forge/permission_sets/new.html.erb +2 -2
- data/app/views/permission_forge/permission_sets/review.html.erb +13 -3
- data/app/views/permission_forge/permission_sets/show.html.erb +7 -5
- data/app/views/permission_forge/resource_actions/_across_sets.html.erb +7 -10
- data/app/views/permission_forge/resource_actions/_effect.html.erb +5 -5
- data/app/views/permission_forge/resource_actions/_form.html.erb +1 -1
- data/app/views/permission_forge/resource_actions/_managed_note.html.erb +5 -3
- data/app/views/permission_forge/resource_actions/confirm_destroy.html.erb +1 -1
- data/app/views/permission_forge/resource_actions/new.html.erb +1 -1
- data/app/views/permission_forge/resource_actions/show.html.erb +2 -2
- data/app/views/permission_forge/resource_fields/_explain.html.erb +19 -15
- data/app/views/permission_forge/resource_fields/show.html.erb +21 -12
- data/app/views/permission_forge/resources/_actions_tab.html.erb +4 -4
- data/app/views/permission_forge/resources/_fields_tab.html.erb +17 -9
- data/app/views/permission_forge/resources/_rules_tab.html.erb +9 -6
- data/app/views/permission_forge/resources/_sync.html.erb +8 -5
- data/app/views/permission_forge/resources/confirm_destroy.html.erb +3 -3
- data/app/views/permission_forge/resources/new.html.erb +3 -2
- data/app/views/permission_forge/resources/show.html.erb +6 -6
- data/app/views/permission_forge/settings/_row.html.erb +2 -2
- data/app/views/permission_forge/settings/show.html.erb +10 -1
- data/app/views/permission_forge/shared/_across_sets.html.erb +8 -9
- data/app/views/permission_forge/shared/_audit_events.html.erb +2 -2
- data/app/views/permission_forge/shared/_audit_footer.html.erb +13 -2
- data/app/views/permission_forge/shared/_crud_cell.html.erb +6 -6
- data/app/views/permission_forge/shared/_mode_banners.html.erb +1 -1
- data/app/views/permission_forge/shared/_savebar.html.erb +4 -2
- data/config/locales/permission_forge.en.yml +480 -69
- data/config/locales/permission_forge.mk.yml +499 -79
- data/config/locales/permission_forge.sq.yml +509 -80
- data/config/routes.rb +38 -23
- data/lib/generators/permission_forge/install/install_generator.rb +21 -0
- data/lib/generators/permission_forge/install/templates/initializer.rb.tt +35 -34
- data/lib/generators/permission_forge/install/templates/migration.rb.tt +5 -5
- data/lib/permission_forge/activation_conditions.rb +200 -0
- data/lib/permission_forge/admin/action_label.rb +33 -0
- data/lib/permission_forge/admin/action_rows.rb +3 -1
- data/lib/permission_forge/admin/actor_label.rb +52 -0
- data/lib/permission_forge/admin/attention_items.rb +152 -31
- data/lib/permission_forge/admin/audit_log.rb +4 -7
- data/lib/permission_forge/admin/audit_trail.rb +33 -17
- data/lib/permission_forge/admin/bulk_people_matcher.rb +32 -14
- data/lib/permission_forge/admin/chain_entries.rb +42 -0
- data/lib/permission_forge/admin/change_set.rb +12 -1
- data/lib/permission_forge/admin/change_set_parts/base.rb +5 -4
- data/lib/permission_forge/admin/change_set_parts/context.rb +3 -1
- data/lib/permission_forge/admin/change_set_parts/field_edit.rb +4 -1
- data/lib/permission_forge/admin/change_set_parts/field_levels.rb +1 -1
- data/lib/permission_forge/admin/change_set_parts/new_actions.rb +1 -1
- data/lib/permission_forge/admin/change_set_parts/new_fields.rb +38 -11
- data/lib/permission_forge/admin/change_set_parts/resource_edit.rb +17 -1
- data/lib/permission_forge/admin/change_set_parts/resource_flags.rb +1 -1
- data/lib/permission_forge/admin/change_set_parts/set_attrs.rb +4 -0
- data/lib/permission_forge/admin/change_set_parts/values.rb +10 -4
- data/lib/permission_forge/admin/column_fields.rb +22 -21
- data/lib/permission_forge/admin/conditions_form.rb +47 -15
- data/lib/permission_forge/admin/controller_catalog.rb +42 -14
- data/lib/permission_forge/admin/editable_lock.rb +83 -3
- data/lib/permission_forge/admin/effective_access.rb +147 -48
- data/lib/permission_forge/admin/effective_fields.rb +75 -67
- data/lib/permission_forge/admin/error_sentence.rb +47 -0
- data/lib/permission_forge/admin/expiry_presets.rb +17 -0
- data/lib/permission_forge/admin/exports/holdings.rb +6 -3
- data/lib/permission_forge/admin/field_across_sets.rb +7 -4
- data/lib/permission_forge/admin/field_chain.rb +109 -0
- data/lib/permission_forge/admin/field_detail.rb +24 -11
- data/lib/permission_forge/admin/field_label.rb +109 -0
- data/lib/permission_forge/admin/field_level.rb +4 -68
- data/lib/permission_forge/admin/field_provenance.rb +132 -0
- data/lib/permission_forge/admin/field_rows.rb +26 -11
- data/lib/permission_forge/admin/last_changes.rb +14 -0
- data/lib/permission_forge/admin/list_sentence.rb +32 -0
- data/lib/permission_forge/admin/mask_strategy_choices.rb +57 -0
- data/lib/permission_forge/admin/model_catalog.rb +83 -39
- data/lib/permission_forge/admin/model_label.rb +66 -0
- data/lib/permission_forge/admin/people_candidates.rb +21 -9
- data/lib/permission_forge/admin/people_directory.rb +23 -5
- data/lib/permission_forge/admin/permission_set_factory.rb +6 -15
- data/lib/permission_forge/admin/person_label.rb +105 -16
- data/lib/permission_forge/admin/prefill.rb +92 -0
- data/lib/permission_forge/admin/record_name.rb +45 -0
- data/lib/permission_forge/admin/rule_builder.rb +77 -3
- data/lib/permission_forge/admin/rule_form.rb +44 -10
- data/lib/permission_forge/admin/rule_form_input.rb +27 -11
- data/lib/permission_forge/admin/rule_notes.rb +43 -0
- data/lib/permission_forge/admin/rule_preview.rb +96 -0
- data/lib/permission_forge/admin/sensitive_columns.rb +10 -62
- data/lib/permission_forge/admin/settings_catalog.rb +64 -12
- data/lib/permission_forge/admin/state_label.rb +97 -0
- data/lib/permission_forge/admin_method_override.rb +44 -0
- data/lib/permission_forge/api/schema.rb +322 -0
- data/lib/permission_forge/api/serializers/base_serializer.rb +87 -1
- data/lib/permission_forge/api/serializers/permission_set_group_user_serializer.rb +1 -1
- data/lib/permission_forge/api/serializers/permission_set_rule_serializer.rb +1 -1
- data/lib/permission_forge/api/serializers/permission_set_serializer.rb +5 -2
- data/lib/permission_forge/api/serializers/permission_set_user_serializer.rb +1 -1
- data/lib/permission_forge/api/serializers/resource_action_serializer.rb +1 -1
- data/lib/permission_forge/api/serializers/resource_field_serializer.rb +1 -1
- data/lib/permission_forge/api/serializers/resource_serializer.rb +1 -1
- data/lib/permission_forge/api_authorizations.rb +59 -0
- data/lib/permission_forge/api_only_admin.rb +81 -0
- data/lib/permission_forge/audit/actor_codes.rb +32 -0
- data/lib/permission_forge/audit/actor_resolver.rb +1 -2
- data/lib/permission_forge/audit/boot_warnings.rb +29 -9
- data/lib/permission_forge/audit/event_catalog.rb +16 -6
- data/lib/permission_forge/audit/event_query.rb +3 -3
- data/lib/permission_forge/audit/reason_codes.rb +25 -0
- data/lib/permission_forge/audit/runtime.rb +9 -2
- data/lib/permission_forge/audit/settings_observer.rb +2 -2
- data/lib/permission_forge/audit/unmasked_without_user.rb +169 -0
- data/lib/permission_forge/audit.rb +24 -10
- data/lib/permission_forge/boot_checks.rb +180 -63
- data/lib/permission_forge/cache/base.rb +10 -0
- data/lib/permission_forge/cache/null_cache.rb +5 -0
- data/lib/permission_forge/cache/outcome.rb +119 -0
- data/lib/permission_forge/cache/rails_cache.rb +6 -0
- data/lib/permission_forge/cache/report.rb +150 -0
- data/lib/permission_forge/cache/user_keys.rb +8 -4
- data/lib/permission_forge/cache_warnings.rb +140 -0
- data/lib/permission_forge/concerns/auto_permission_scoping.rb +19 -2
- data/lib/permission_forge/concerns/has_tenant_scoping.rb +1 -1
- data/lib/permission_forge/concerns/has_uuid.rb +1 -1
- data/lib/permission_forge/concerns/keeps_copied_values.rb +48 -0
- data/lib/permission_forge/concerns/masked_attributes/alias_access.rb +12 -2
- data/lib/permission_forge/concerns/masked_attributes/virtual_readers.rb +6 -4
- data/lib/permission_forge/concerns/masked_attributes.rb +150 -34
- data/lib/permission_forge/concerns/normalizes_label.rb +78 -0
- data/lib/permission_forge/concerns/permission_cache_accessor.rb +3 -3
- data/lib/permission_forge/concerns/permission_scoped_findable.rb +121 -29
- data/lib/permission_forge/concerns/permissioned_user.rb +1 -1
- data/lib/permission_forge/concerns/records_audit_events.rb +13 -6
- data/lib/permission_forge/concerns/requires_name.rb +37 -0
- data/lib/permission_forge/concerns/validates_column_lengths.rb +24 -0
- data/lib/permission_forge/concerns/validates_dependency.rb +5 -2
- data/lib/permission_forge/concerns/validates_required_columns.rb +74 -0
- data/lib/permission_forge/conditional_sets.rb +13 -6
- data/lib/permission_forge/configuration/admin_ui_settings.rb +18 -4
- data/lib/permission_forge/configuration/audit_settings.rb +2 -1
- data/lib/permission_forge/configuration/controller_action_settings.rb +5 -4
- data/lib/permission_forge/configuration/masked_attributes_settings.rb +73 -19
- data/lib/permission_forge/configuration/presets.rb +20 -11
- data/lib/permission_forge/configuration/query_scoping_settings.rb +55 -0
- data/lib/permission_forge/configuration/strategy_values.rb +2 -1
- data/lib/permission_forge/configuration.rb +41 -18
- data/lib/permission_forge/controller_actions/managed_index.rb +8 -7
- data/lib/permission_forge/database_reach.rb +93 -0
- data/lib/permission_forge/deprecation.rb +1 -1
- data/lib/permission_forge/docs/api_samples.rb +250 -0
- data/lib/permission_forge/docs/configuration_reference.rb +172 -0
- data/lib/permission_forge/encrypted_attributes.rb +150 -0
- data/lib/permission_forge/engine.rb +46 -14
- data/lib/permission_forge/execution_state/restore.rb +74 -0
- data/lib/permission_forge/execution_state.rb +8 -1
- data/lib/permission_forge/field_level.rb +123 -0
- data/lib/permission_forge/field_names.rb +145 -0
- data/lib/permission_forge/field_variants.rb +96 -0
- data/lib/permission_forge/generated_text.rb +95 -0
- data/lib/permission_forge/inspection_filters.rb +83 -0
- data/lib/permission_forge/managed_fields.rb +66 -0
- data/lib/permission_forge/masked_models.rb +25 -19
- data/lib/permission_forge/masking_sync.rb +100 -25
- data/lib/permission_forge/model_bindings.rb +30 -19
- data/lib/permission_forge/model_lookup.rb +42 -0
- data/lib/permission_forge/operators/encrypted_alias.rb +67 -0
- data/lib/permission_forge/operators/registry.rb +6 -0
- data/lib/permission_forge/operators/rule_column.rb +242 -27
- data/lib/permission_forge/operators/rule_path.rb +179 -0
- data/lib/permission_forge/operators/rule_problem.rb +309 -0
- data/lib/permission_forge/operators/rule_sources.rb +96 -0
- data/lib/permission_forge/operators/value_casters/postgresql_types.rb +237 -0
- data/lib/permission_forge/operators/value_casters/temporal.rb +9 -20
- data/lib/permission_forge/operators/value_casters.rb +61 -13
- data/lib/permission_forge/operators/value_typing.rb +83 -7
- data/lib/permission_forge/query_scoping_warnings.rb +22 -17
- data/lib/permission_forge/rake_audit.rb +5 -4
- data/lib/permission_forge/rake_dry_run.rb +32 -0
- data/lib/permission_forge/rake_output.rb +189 -0
- data/lib/permission_forge/record_read_access.rb +51 -0
- data/lib/permission_forge/resource_chain.rb +14 -2
- data/lib/permission_forge/schema_read/ordered_types.rb +72 -0
- data/lib/permission_forge/schema_read.rb +335 -0
- data/lib/permission_forge/secret_columns.rb +49 -0
- data/lib/permission_forge/sensitive_columns.rb +114 -0
- data/lib/permission_forge/services/attribute_access_guard.rb +22 -14
- data/lib/permission_forge/services/bulk_permission_set_assignment.rb +16 -8
- data/lib/permission_forge/services/concerns/clonable.rb +9 -0
- data/lib/permission_forge/services/concerns/settles_cache.rb +58 -0
- data/lib/permission_forge/services/consent_withdrawal_handler.rb +37 -19
- data/lib/permission_forge/services/data_subject_erasure.rb +83 -45
- data/lib/permission_forge/services/expired_assignment_cleaner.rb +62 -25
- data/lib/permission_forge/services/extra_field_defaults.rb +1 -1
- data/lib/permission_forge/services/field_permission_merger.rb +44 -17
- data/lib/permission_forge/services/filters_response_attributes.rb +20 -5
- data/lib/permission_forge/services/gdpr_field_auditor/text_report.rb +33 -5
- data/lib/permission_forge/services/gdpr_field_auditor.rb +68 -48
- data/lib/permission_forge/services/host_key_guard.rb +8 -5
- data/lib/permission_forge/services/mask_value.rb +72 -11
- data/lib/permission_forge/services/permission_cache_clearer.rb +36 -15
- data/lib/permission_forge/services/permission_hash_builder.rb +48 -19
- data/lib/permission_forge/services/permission_rule_applier.rb +131 -68
- data/lib/permission_forge/services/permission_set_cloner.rb +32 -8
- data/lib/permission_forge/services/permission_set_condition_evaluator.rb +38 -36
- data/lib/permission_forge/services/permission_set_group_cloner.rb +2 -1
- data/lib/permission_forge/services/permits_attributes/result.rb +31 -0
- data/lib/permission_forge/services/permits_attributes.rb +48 -27
- data/lib/permission_forge/services/resource_field_generator/row_builder.rb +96 -0
- data/lib/permission_forge/services/resource_field_generator.rb +71 -42
- data/lib/permission_forge/services/resource_with_fields_builder.rb +2 -1
- data/lib/permission_forge/services/rls_operator_sql.rb +83 -24
- data/lib/permission_forge/services/rls_policy_generator.rb +83 -39
- data/lib/permission_forge/services/rls_read_access_sql.rb +131 -0
- data/lib/permission_forge/services/security_audit/action_check.rb +49 -5
- data/lib/permission_forge/services/security_audit/assignment_check.rb +2 -1
- data/lib/permission_forge/services/security_audit/audit_trail_check.rb +1 -3
- data/lib/permission_forge/services/security_audit/conditions_check.rb +92 -0
- data/lib/permission_forge/services/security_audit/config_check.rb +41 -18
- data/lib/permission_forge/services/security_audit/integrity_check.rb +71 -4
- data/lib/permission_forge/services/security_audit/masking_without_user.rb +66 -0
- data/lib/permission_forge/services/security_audit/personal_data_check.rb +7 -11
- data/lib/permission_forge/services/security_audit/rule_check.rb +180 -50
- data/lib/permission_forge/services/security_audit/text_report.rb +14 -2
- data/lib/permission_forge/services/security_audit/unmanaged_actions.rb +145 -0
- data/lib/permission_forge/services/security_audit/user_model_check.rb +4 -3
- data/lib/permission_forge/services/security_audit.rb +16 -4
- data/lib/permission_forge/services/stale_assignment_auditor.rb +8 -5
- data/lib/permission_forge/timestamp_fields.rb +60 -0
- data/lib/permission_forge/unique_conflict.rb +117 -0
- data/lib/permission_forge/version.rb +1 -1
- data/lib/permission_forge/virtual_fields/discovery.rb +2 -1
- data/lib/permission_forge/virtual_fields.rb +15 -5
- data/lib/permission_forge/warnings.rb +263 -0
- data/lib/permission_forge/web_request.rb +248 -0
- data/lib/permission_forge.rb +54 -0
- data/lib/tasks/permission_forge.rake +26 -6
- data/lib/tasks/permission_forge_audit_events.rake +8 -4
- data/lib/tasks/permission_forge_cache.rake +12 -12
- data/lib/tasks/permission_forge_docs.rake +48 -0
- data/lib/tasks/permission_forge_expiration.rake +11 -5
- data/lib/tasks/permission_forge_gdpr.rake +36 -20
- metadata +84 -1
|
@@ -4,19 +4,48 @@ module PermissionForge
|
|
|
4
4
|
# @api private
|
|
5
5
|
module Api
|
|
6
6
|
module Serializers
|
|
7
|
+
# The JSON API's built-in serializers: `attributes` lists the keys of
|
|
8
|
+
# an answer, in order; each is the serializer's own (private) method
|
|
9
|
+
# of that name, else the record's.
|
|
10
|
+
#
|
|
11
|
+
# A key that refers to another PermissionForge record is declared with
|
|
12
|
+
# {.reference}: the record's numeric id (`permission_set_id`, as
|
|
13
|
+
# through 0.14.1) followed by its uuid (`permission_set_uuid`), so a
|
|
14
|
+
# client that only knows uuids — `api_record_identifier = :uuid`, the
|
|
15
|
+
# identifier the URLs take — can follow it (field test round 8, D#29).
|
|
16
|
+
# Both keys are always there, whatever the identifier setting. The uuid
|
|
17
|
+
# key is answer-only: no endpoint takes it (the `_id` key changes a
|
|
18
|
+
# reference, and takes the uuid under `:uuid`), so a whole answer sent
|
|
19
|
+
# back is ignored there. A collection looks the uuids up in one query
|
|
20
|
+
# per reference.
|
|
7
21
|
class BaseSerializer
|
|
8
22
|
def initialize(record)
|
|
9
23
|
@record = record
|
|
10
24
|
end
|
|
11
25
|
|
|
26
|
+
# @return [Hash{Symbol => Object}] the record's keys, in order
|
|
12
27
|
def as_json(*)
|
|
13
28
|
self.class.attribute_names.to_h do |attr|
|
|
14
29
|
[attr, respond_to?(attr, true) ? send(attr) : @record.public_send(attr)]
|
|
15
30
|
end
|
|
16
31
|
end
|
|
17
32
|
|
|
33
|
+
# The uuids of the records the references name, as {.reference_uuids}
|
|
34
|
+
# answers them (a collection passes the lookup of all its records).
|
|
35
|
+
#
|
|
36
|
+
# @param uuids [Hash{Symbol => Hash{Integer => String}}]
|
|
37
|
+
# @return [self]
|
|
38
|
+
def with_reference_uuids(uuids)
|
|
39
|
+
@reference_uuids = uuids
|
|
40
|
+
self
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @param records [Enumerable<ActiveRecord::Base>]
|
|
44
|
+
# @return [Array<Hash>]
|
|
18
45
|
def self.collection(records)
|
|
19
|
-
records
|
|
46
|
+
records = records.to_a
|
|
47
|
+
uuids = reference_uuids(records)
|
|
48
|
+
records.map { |r| new(r).with_reference_uuids(uuids).as_json }
|
|
20
49
|
end
|
|
21
50
|
|
|
22
51
|
def self.attributes(*attrs)
|
|
@@ -27,9 +56,66 @@ module PermissionForge
|
|
|
27
56
|
@attribute_names || []
|
|
28
57
|
end
|
|
29
58
|
|
|
59
|
+
# The two keys of a reference to another PermissionForge record, for
|
|
60
|
+
# `attributes`: `<association>_id` (its numeric id) and
|
|
61
|
+
# `<association>_uuid` (its uuid, read through the association's
|
|
62
|
+
# model; nil when the id is nil or names no record).
|
|
63
|
+
#
|
|
64
|
+
# @param association [Symbol] a belongs_to of the serialized model
|
|
65
|
+
# @return [Array(Symbol, Symbol)]
|
|
66
|
+
def self.reference(association)
|
|
67
|
+
id_key, uuid_key = reference_keys(association)
|
|
68
|
+
own_references << association
|
|
69
|
+
define_method(uuid_key) { reference_uuid(association, id_key) }
|
|
70
|
+
private uuid_key
|
|
71
|
+
[id_key, uuid_key]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# The one naming of a reference's keys: the id key and the uuid key.
|
|
75
|
+
#
|
|
76
|
+
# @param association [Symbol, String]
|
|
77
|
+
# @return [Array(Symbol, Symbol)]
|
|
78
|
+
def self.reference_keys(association)
|
|
79
|
+
[:"#{association}_id", :"#{association}_uuid"]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @return [Array<Symbol>] the associations declared with {.reference}, a superclass's too
|
|
83
|
+
def self.references
|
|
84
|
+
inherited = superclass.respond_to?(:references) ? superclass.references : []
|
|
85
|
+
inherited | own_references
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# The uuid of every record the references of `records` name, one
|
|
89
|
+
# query per reference (the referenced model's default scope, the
|
|
90
|
+
# tenant's, applies as it does to the association).
|
|
91
|
+
#
|
|
92
|
+
# @param records [Array<ActiveRecord::Base>]
|
|
93
|
+
# @return [Hash{Symbol => Hash{Integer => String}}] association => { id => uuid }
|
|
94
|
+
def self.reference_uuids(records)
|
|
95
|
+
references.to_h { |association| [association, uuids_named(records, association)] }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def self.own_references
|
|
99
|
+
@own_references ||= []
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def self.uuids_named(records, association)
|
|
103
|
+
id_key, = reference_keys(association)
|
|
104
|
+
ids = records.filter_map { |record| record.public_send(id_key) }.uniq
|
|
105
|
+
return {} if ids.empty?
|
|
106
|
+
|
|
107
|
+
records.first.class.reflect_on_association(association).klass.where(id: ids).pluck(:id, :uuid).to_h
|
|
108
|
+
end
|
|
109
|
+
private_class_method :own_references, :uuids_named
|
|
110
|
+
|
|
30
111
|
private
|
|
31
112
|
|
|
32
113
|
attr_reader :record
|
|
114
|
+
|
|
115
|
+
def reference_uuid(association, id_key)
|
|
116
|
+
@reference_uuids ||= self.class.reference_uuids([record])
|
|
117
|
+
@reference_uuids.fetch(association, {})[record.public_send(id_key)]
|
|
118
|
+
end
|
|
33
119
|
end
|
|
34
120
|
end
|
|
35
121
|
end
|
|
@@ -4,7 +4,7 @@ module PermissionForge
|
|
|
4
4
|
module Api
|
|
5
5
|
module Serializers
|
|
6
6
|
class PermissionSetGroupUserSerializer < BaseSerializer
|
|
7
|
-
attributes :id, :uuid, :
|
|
7
|
+
attributes :id, :uuid, *reference(:permission_set_group), :user_id, :created_at, :updated_at
|
|
8
8
|
end
|
|
9
9
|
end
|
|
10
10
|
end
|
|
@@ -6,7 +6,7 @@ module PermissionForge
|
|
|
6
6
|
class PermissionSetRuleSerializer < BaseSerializer
|
|
7
7
|
REDACTED_PLACEHOLDER = '[REDACTED]'
|
|
8
8
|
|
|
9
|
-
attributes :id, :uuid, :
|
|
9
|
+
attributes :id, :uuid, *reference(:permission_set), *reference(:resource),
|
|
10
10
|
:name, :failure_message, :join_type,
|
|
11
11
|
:field_name, :field_value, :field_accessor_path,
|
|
12
12
|
:operator, :include_nulls, :is_active, :is_editable,
|
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
module PermissionForge
|
|
4
4
|
module Api
|
|
5
5
|
module Serializers
|
|
6
|
+
# `allowed_ips` is the stored text (commas between the entries since
|
|
7
|
+
# 0.15); `allowed_ip_list` the entries as the engine reads them
|
|
8
|
+
# (ActivationConditions) — what a client should write back.
|
|
6
9
|
class PermissionSetSerializer < BaseSerializer
|
|
7
10
|
attributes :id, :uuid, :name, :description, :is_active, :is_editable, :is_muting,
|
|
8
|
-
:requires_session_flag, :allowed_ips, :allowed_hours_start, :allowed_hours_end,
|
|
9
|
-
:
|
|
11
|
+
:requires_session_flag, :allowed_ips, :allowed_ip_list, :allowed_hours_start, :allowed_hours_end,
|
|
12
|
+
*reference(:permission_set_group), :created_at, :updated_at
|
|
10
13
|
end
|
|
11
14
|
end
|
|
12
15
|
end
|
|
@@ -4,7 +4,7 @@ module PermissionForge
|
|
|
4
4
|
module Api
|
|
5
5
|
module Serializers
|
|
6
6
|
class PermissionSetUserSerializer < BaseSerializer
|
|
7
|
-
attributes :id, :uuid, :
|
|
7
|
+
attributes :id, :uuid, *reference(:permission_set), :user_id, :expires_at, :created_at, :updated_at
|
|
8
8
|
end
|
|
9
9
|
end
|
|
10
10
|
end
|
|
@@ -4,7 +4,7 @@ module PermissionForge
|
|
|
4
4
|
module Api
|
|
5
5
|
module Serializers
|
|
6
6
|
class ResourceActionSerializer < BaseSerializer
|
|
7
|
-
attributes :id, :uuid, :
|
|
7
|
+
attributes :id, :uuid, *reference(:resource), :name, :label, :description,
|
|
8
8
|
:is_permitted, :is_active,
|
|
9
9
|
:created_at, :updated_at
|
|
10
10
|
end
|
|
@@ -4,7 +4,7 @@ module PermissionForge
|
|
|
4
4
|
module Api
|
|
5
5
|
module Serializers
|
|
6
6
|
class ResourceFieldSerializer < BaseSerializer
|
|
7
|
-
attributes :id, :uuid, :
|
|
7
|
+
attributes :id, :uuid, *reference(:resource), :name, :label, :description,
|
|
8
8
|
:is_active, :is_editable,
|
|
9
9
|
:read_access, :edit_access, :mask_access, :mask_strategy,
|
|
10
10
|
:created_at, :updated_at
|
|
@@ -4,7 +4,7 @@ module PermissionForge
|
|
|
4
4
|
module Api
|
|
5
5
|
module Serializers
|
|
6
6
|
class ResourceSerializer < BaseSerializer
|
|
7
|
-
attributes :id, :uuid, :
|
|
7
|
+
attributes :id, :uuid, *reference(:permission_set), :name, :description,
|
|
8
8
|
:is_active, :is_editable,
|
|
9
9
|
:read_access, :create_access, :edit_access, :delete_access,
|
|
10
10
|
:view_all, :modify_all,
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
# The JSON API's authorization lambdas, and what the API refuses while one
|
|
5
|
+
# it needs is unset. `api_auth` proves identity only: while
|
|
6
|
+
# api_require_authorization is on (the default) a change needs
|
|
7
|
+
# api_action_authorization and an assignment api_assignment_authorization,
|
|
8
|
+
# and while api_stats_require_admin is on (the default) GET /api/stats
|
|
9
|
+
# needs api_stats_authorization. Without one, the API answers 403 and the
|
|
10
|
+
# answer names the option: it fails closed, but nothing said so before the
|
|
11
|
+
# first refused request (field test round 8, D#7). The boot note
|
|
12
|
+
# (BootChecks, :api_authorization_unset) and the audit finding
|
|
13
|
+
# (ConfigCheck#api_authorization_unset) both read `unset`.
|
|
14
|
+
#
|
|
15
|
+
# The stats note cannot see an api_auth that still sets the deprecated
|
|
16
|
+
# 0.14 `@admin = true` on the controller (Api::DashboardController
|
|
17
|
+
# #legacy_admin_flag?, until 0.16): that host is not refused, and the note
|
|
18
|
+
# says so.
|
|
19
|
+
#
|
|
20
|
+
# api_require_authorization = false with a lambda unset is another
|
|
21
|
+
# condition (every caller api_auth admits may change permissions: the
|
|
22
|
+
# api_authorization_gap warning, the api_mutations_unguarded finding).
|
|
23
|
+
#
|
|
24
|
+
# @api private
|
|
25
|
+
module ApiAuthorizations
|
|
26
|
+
# Each lambda => [the setting under which the API needs it, what the API
|
|
27
|
+
# refuses (403) while it is unset].
|
|
28
|
+
LAMBDAS = {
|
|
29
|
+
api_action_authorization: [:api_require_authorization, 'every create, update, delete and copy'],
|
|
30
|
+
api_assignment_authorization: [:api_require_authorization, 'every assignment and group membership change'],
|
|
31
|
+
api_stats_authorization: [:api_stats_require_admin,
|
|
32
|
+
'GET /api/stats (or set api_stats_require_admin = false; an api_auth that sets the ' \
|
|
33
|
+
'deprecated @admin = true still passes until 0.16)']
|
|
34
|
+
}.freeze
|
|
35
|
+
|
|
36
|
+
module_function
|
|
37
|
+
|
|
38
|
+
# The lambdas the API needs and does not have. None while the API is off
|
|
39
|
+
# or api_auth is unset (every request is then a 401).
|
|
40
|
+
#
|
|
41
|
+
# @param config [Configuration]
|
|
42
|
+
# @return [Hash{Symbol => String}] lambda option => what the API refuses without it
|
|
43
|
+
def unset(config)
|
|
44
|
+
return {} unless config.api_enabled && config.api_auth
|
|
45
|
+
|
|
46
|
+
LAMBDAS.each_with_object({}) do |(option, (required_by, refused)), unset|
|
|
47
|
+
unset[option] = refused if config.public_send(required_by) && !config.public_send(option)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# One line per unset lambda: "<option>: <what is refused>".
|
|
52
|
+
#
|
|
53
|
+
# @param config [Configuration]
|
|
54
|
+
# @return [Array<String>]
|
|
55
|
+
def lines(config)
|
|
56
|
+
unset(config).map { |option, refused| "#{option}: #{refused}" }
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
# The admin screens in an API-only application (`rails new --api`,
|
|
5
|
+
# `config.api_only = true`). Rails then leaves ActionDispatch::Cookies, the
|
|
6
|
+
# session store and ActionDispatch::Flash out of the middleware stack, and
|
|
7
|
+
# every admin page that renders a form raises: NameError for `flash` (its
|
|
8
|
+
# request methods are loaded with the middleware), then
|
|
9
|
+
# ActionDispatch::Request::Session::DisabledSessionError for the CSRF
|
|
10
|
+
# token. The JSON API needs none of them. BootChecks warns through
|
|
11
|
+
# Warnings.emit (:api_only_admin), naming the middlewares
|
|
12
|
+
# config/application.rb must add, when the engine is mounted — once the
|
|
13
|
+
# routes are drawn (the engine's config.after_routes_loaded), and not when
|
|
14
|
+
# config.api_only_admin_warning is false (an app that uses only the API).
|
|
15
|
+
#
|
|
16
|
+
# @api private
|
|
17
|
+
module ApiOnlyAdmin
|
|
18
|
+
COOKIES = 'ActionDispatch::Cookies'
|
|
19
|
+
SESSION = 'ActionDispatch::Session::CookieStore'
|
|
20
|
+
FLASH = 'ActionDispatch::Flash'
|
|
21
|
+
# What each one is recognised by in the stack: any session store counts.
|
|
22
|
+
PRESENT = {
|
|
23
|
+
COOKIES => ->(name) { name == COOKIES },
|
|
24
|
+
SESSION => ->(name) { name.start_with?('ActionDispatch::Session::') },
|
|
25
|
+
FLASH => ->(name) { name == FLASH }
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
module_function
|
|
29
|
+
|
|
30
|
+
# @param application [Rails::Application]
|
|
31
|
+
# @return [Array<String>] the middlewares the admin needs that the stack
|
|
32
|
+
# lacks; empty unless the application is api_only with the engine mounted
|
|
33
|
+
def missing_middlewares(application)
|
|
34
|
+
return [] unless api_only?(application) && mounted?(application)
|
|
35
|
+
|
|
36
|
+
names = middleware_names(application)
|
|
37
|
+
return [] if names.nil?
|
|
38
|
+
|
|
39
|
+
PRESENT.reject { |_name, present| names.any?(&present) }.keys
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def api_only?(application)
|
|
43
|
+
application.respond_to?(:config) && application.config.respond_to?(:api_only) && application.config.api_only == true
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @param missing [Array<String>]
|
|
47
|
+
# @return [String] the warning
|
|
48
|
+
def message(missing)
|
|
49
|
+
'The application is API-only (config.api_only = true) and PermissionForge::Engine is mounted: the admin screens ' \
|
|
50
|
+
"need #{missing.join(', ')} in the middleware stack, which an API-only application leaves out. Every admin page " \
|
|
51
|
+
"that renders a form answers 500 until config/application.rb adds #{missing.map { |name| "config.middleware.use #{name}" }.join(', ')} " \
|
|
52
|
+
'(docs/admin-ui.md#api-only-applications). The JSON API works without them.'
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# The names in the built stack (after initialize!); nil while it is still
|
|
56
|
+
# the configuration proxy, when nothing can be said yet.
|
|
57
|
+
def middleware_names(application)
|
|
58
|
+
stack = application.middleware
|
|
59
|
+
return nil unless stack.respond_to?(:middlewares)
|
|
60
|
+
|
|
61
|
+
stack.middlewares.map { |middleware| middleware.name.to_s }
|
|
62
|
+
rescue StandardError
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Whether the engine is mounted in the routes as drawn. Asked once they
|
|
67
|
+
# are (after_routes_loaded: at boot, or at their first use where Rails 8
|
|
68
|
+
# draws them lazily); it never draws them itself — a draw at boot is
|
|
69
|
+
# undone and drawn again at the first request. When they cannot be read,
|
|
70
|
+
# yes (a warning too many beats a 500 nobody was told about).
|
|
71
|
+
def mounted?(application)
|
|
72
|
+
application.routes.routes.any? { |route| engine?(route.app) }
|
|
73
|
+
rescue StandardError
|
|
74
|
+
true
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def engine?(app)
|
|
78
|
+
app == PermissionForge::Engine || (app.respond_to?(:app) && app.app == PermissionForge::Engine)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Audit
|
|
5
|
+
# The actor labels the gem writes on an event itself, stored as codes
|
|
6
|
+
# (never English), each for one actor type: the admin names them from
|
|
7
|
+
# the locale (`permission_forge.audit_log.actor_codes.<code>`,
|
|
8
|
+
# Admin::ActorLabel). A label a person or the host chose is shown as
|
|
9
|
+
# stored; so is a code under another actor type (a person called "boot").
|
|
10
|
+
#
|
|
11
|
+
# @api private
|
|
12
|
+
module ActorCodes
|
|
13
|
+
# An API request nobody named (api_auth said yes, but not who): ActorResolver's fallback.
|
|
14
|
+
API_CLIENT = 'api_client'
|
|
15
|
+
# The settings observer, which records the audit settings as the application boots.
|
|
16
|
+
BOOT = 'boot'
|
|
17
|
+
# actor type => the codes it carries.
|
|
18
|
+
BY_TYPE = { 'api' => [API_CLIENT], 'system' => [BOOT] }.freeze
|
|
19
|
+
# A label stored before it was a code => the code.
|
|
20
|
+
LEGACY = { 'API client' => API_CLIENT }.freeze
|
|
21
|
+
|
|
22
|
+
# @param type [String, Symbol] the event's actor_type
|
|
23
|
+
# @param label [String, nil] the event's actor_label
|
|
24
|
+
# @return [String, nil] the code the label stands for, nil for any other label
|
|
25
|
+
def self.code(type, label)
|
|
26
|
+
code = label.to_s.strip
|
|
27
|
+
code = LEGACY.fetch(code, code)
|
|
28
|
+
code if BY_TYPE.fetch(type.to_s, []).include?(code)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -25,7 +25,6 @@ module PermissionForge
|
|
|
25
25
|
# username it has just verified. Not reachable from a header (those are
|
|
26
26
|
# HTTP_*).
|
|
27
27
|
BASIC_AUTH_ENV = 'permission_forge.audit.basic_auth_user'
|
|
28
|
-
API_CLIENT_LABEL = 'API client'
|
|
29
28
|
|
|
30
29
|
# The steps tried, in order, per request source; the first that names
|
|
31
30
|
# someone wins.
|
|
@@ -37,7 +36,7 @@ module PermissionForge
|
|
|
37
36
|
# Who acts when no step names anyone.
|
|
38
37
|
FALLBACKS = {
|
|
39
38
|
'admin_ui' => -> { Actor.unknown },
|
|
40
|
-
'api' => -> { Actor.new(type: 'api', label:
|
|
39
|
+
'api' => -> { Actor.new(type: 'api', label: ActorCodes::API_CLIENT) }
|
|
41
40
|
}.freeze
|
|
42
41
|
|
|
43
42
|
# @param controller [ActionController::Metal]
|
|
@@ -5,23 +5,34 @@ module PermissionForge
|
|
|
5
5
|
# The audit boot warnings, written by the engine once the database is
|
|
6
6
|
# reachable. The missing-table warning is logged by Audit (once per
|
|
7
7
|
# process, by Audit.verify_table!) with the text of .missing_table; this
|
|
8
|
-
# adds auditing switched off and one shared login for every change
|
|
8
|
+
# adds auditing switched off and one shared login for every change, each
|
|
9
|
+
# through PermissionForge::Warnings.emit.
|
|
10
|
+
#
|
|
11
|
+
# The table is not checked while a generator runs (`bin/rails generate
|
|
12
|
+
# permission_forge:install` boots the app before it writes the migration
|
|
13
|
+
# that creates the table): it is checked again, and the warning logged,
|
|
14
|
+
# the first time an event is recorded. The emitter writes nothing then.
|
|
9
15
|
#
|
|
10
16
|
# @api private
|
|
11
17
|
module BootWarnings
|
|
12
|
-
|
|
13
|
-
|
|
18
|
+
# @param generating [Boolean] whether a Rails generator is running
|
|
19
|
+
def self.call(configuration = PermissionForge.configuration, generating: generator_running?)
|
|
20
|
+
PermissionForge::Audit.verify_table! unless generating
|
|
14
21
|
|
|
15
22
|
if configuration.audit_events == false
|
|
16
|
-
|
|
17
|
-
|
|
23
|
+
PermissionForge::Warnings.emit(:audit_events_off,
|
|
24
|
+
'audit_events is false, so permission changes are not recorded. Set ' \
|
|
25
|
+
'config.audit_events = :auto unless another system records them.',
|
|
26
|
+
severity: :security)
|
|
18
27
|
end
|
|
19
28
|
|
|
20
29
|
return unless configuration.http_basic_auth_enabled && configuration.audit_actor_resolver.nil?
|
|
21
30
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
31
|
+
PermissionForge::Warnings.emit(:audit_actor_shared_login,
|
|
32
|
+
'The admin UI uses HTTP Basic auth and audit_actor_resolver is not set: ' \
|
|
33
|
+
'every change is attributed to the shared login. Set ' \
|
|
34
|
+
'config.audit_actor_resolver to return the signed-in admin.',
|
|
35
|
+
audience: :server)
|
|
25
36
|
end
|
|
26
37
|
|
|
27
38
|
# The missing-table warning (Audit logs it once per process). Its
|
|
@@ -39,10 +50,19 @@ module PermissionForge
|
|
|
39
50
|
else
|
|
40
51
|
'its migration is generated and run (bin/rails generate permission_forge:upgrade, then bin/rails db:migrate)'
|
|
41
52
|
end
|
|
42
|
-
"
|
|
53
|
+
"audit_events is :auto but the table '#{table}' was not found: no audit events are " \
|
|
43
54
|
"recorded until #{remedy}. Changes still work."
|
|
44
55
|
end
|
|
45
56
|
|
|
57
|
+
# `bin/rails generate` or `bin/rails destroy` is booting the app
|
|
58
|
+
# (Warnings.generator_running?: its command class is loaded, not
|
|
59
|
+
# merely Rails::Generators, which a gem may load at boot).
|
|
60
|
+
#
|
|
61
|
+
# @return [Boolean]
|
|
62
|
+
def self.generator_running?
|
|
63
|
+
PermissionForge::Warnings.generator_running?
|
|
64
|
+
end
|
|
65
|
+
|
|
46
66
|
# @return [Boolean] whether a permission_forge migration in the app's
|
|
47
67
|
# migration paths creates the audit events table
|
|
48
68
|
def self.audit_migration_present?
|
|
@@ -15,11 +15,17 @@ module PermissionForge
|
|
|
15
15
|
class UnknownEventError < ArgumentError; end
|
|
16
16
|
|
|
17
17
|
# `notification` is computed once: Audit.instrument asks for it on hot paths.
|
|
18
|
+
# `cache`: the event sums up an operation that invalidated, cleared or
|
|
19
|
+
# removed cached permissions, so its data carries the one Cache::Outcome
|
|
20
|
+
# (`cache_confirmed`, `process_local`, `unconfirmed`).
|
|
18
21
|
#
|
|
19
22
|
# @api private
|
|
20
|
-
Entry = Struct.new(:name, :category, :runtime_group, :notification, keyword_init: true) do
|
|
23
|
+
Entry = Struct.new(:name, :category, :runtime_group, :notification, :cache, keyword_init: true) do
|
|
21
24
|
# Runtime events are published by Audit.instrument and stored per group.
|
|
22
25
|
def runtime? = category == 'access'
|
|
26
|
+
|
|
27
|
+
# Whether the event's data carries a Cache::Outcome.
|
|
28
|
+
def cache? = cache == true
|
|
23
29
|
end
|
|
24
30
|
|
|
25
31
|
CATEGORIES = %w[change admin system access].freeze
|
|
@@ -34,10 +40,13 @@ module PermissionForge
|
|
|
34
40
|
# @param category [String] change, admin, system or access
|
|
35
41
|
# @param runtime_group [Symbol, nil] required for access events, refused
|
|
36
42
|
# for the others
|
|
43
|
+
# @param cache [Boolean] the event's data carries a Cache::Outcome (a
|
|
44
|
+
# cache-touching operation's summary; test/permission_forge/audit/cache_scope_events_test.rb
|
|
45
|
+
# runs every flagged event's operation)
|
|
37
46
|
# @return [Entry]
|
|
38
|
-
def register(name, category:, runtime_group: nil)
|
|
47
|
+
def register(name, category:, runtime_group: nil, cache: false)
|
|
39
48
|
entry = Entry.new(name: name.to_s, category: category.to_s, runtime_group: runtime_group&.to_sym,
|
|
40
|
-
notification: "#{name}.permission_forge").freeze
|
|
49
|
+
notification: "#{name}.permission_forge", cache: cache == true).freeze
|
|
41
50
|
validate!(entry)
|
|
42
51
|
REGISTRY_MUTEX.synchronize { @entries[entry.name] = entry }
|
|
43
52
|
end
|
|
@@ -92,15 +101,16 @@ module PermissionForge
|
|
|
92
101
|
group_membership.added group_membership.changed group_membership.removed field.sensitive_acknowledged]
|
|
93
102
|
.each { |name| register(name, category: 'change') }
|
|
94
103
|
|
|
95
|
-
%w[erasure.performed consent.withdrawn cleanup.expired_assignments cache.cleared
|
|
96
|
-
|
|
104
|
+
%w[erasure.performed consent.withdrawn cleanup.expired_assignments cache.cleared cache.settled]
|
|
105
|
+
.each { |name| register(name, category: 'system', cache: true) }
|
|
106
|
+
%w[audit.purged audit.settings_observed report.generated].each { |name| register(name, category: 'system') }
|
|
97
107
|
|
|
98
108
|
%w[admin.effective_access_viewed audit.viewed audit.exported admin.auth_failed api.auth_failed admin.lock_refused]
|
|
99
109
|
.each { |name| register(name, category: 'admin') }
|
|
100
110
|
|
|
101
111
|
{
|
|
102
112
|
'access.denied' => :denied, 'action.denied' => :denied,
|
|
103
|
-
'rule.bypassed' => :bypass, 'query.unscoped' => :bypass,
|
|
113
|
+
'rule.bypassed' => :bypass, 'query.unscoped' => :bypass, 'field.unmasked_without_user' => :bypass,
|
|
104
114
|
'field.access' => :field_read,
|
|
105
115
|
'params.filtered' => :params_filtered, 'write.masked_skipped' => :params_filtered,
|
|
106
116
|
'permissions.rebuilt' => :cache, 'permissions.invalidated' => :cache
|
|
@@ -60,10 +60,10 @@ module PermissionForge
|
|
|
60
60
|
problem(name, :invalid)
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
-
#
|
|
64
|
-
# 422 bodies of the other endpoints.
|
|
63
|
+
# The gem's own validation messages (permission_forge.validations.<kind>:
|
|
64
|
+
# blank, invalid), like the 422 bodies of the other endpoints.
|
|
65
65
|
def problem(name, kind)
|
|
66
|
-
@errors[name] << I18n.t("
|
|
66
|
+
@errors[name] << I18n.t("permission_forge.validations.#{kind}")
|
|
67
67
|
end
|
|
68
68
|
|
|
69
69
|
# A parameter's value as a String, nil when absent; an array or hash
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PermissionForge
|
|
4
|
+
module Audit
|
|
5
|
+
# The reasons the gem records on an event itself, stored as codes (never
|
|
6
|
+
# English): the admin renders a code from the locale
|
|
7
|
+
# (`permission_forge.audit_log.reason_codes.<code>`, helper
|
|
8
|
+
# `pf_audit_reason`) and any other reason as it was typed. The CSV export
|
|
9
|
+
# and the API keep the stored value.
|
|
10
|
+
#
|
|
11
|
+
# @api private
|
|
12
|
+
module ReasonCodes
|
|
13
|
+
# ExpiredAssignmentCleaner's reason when nobody gave one (rake, the admin's Clean up).
|
|
14
|
+
EXPIRED = 'expired'
|
|
15
|
+
# Every code.
|
|
16
|
+
ALL = [EXPIRED].freeze
|
|
17
|
+
|
|
18
|
+
# @param reason [String, nil] an event's (or a row's) stored reason
|
|
19
|
+
# @return [Boolean] whether it is one of the gem's codes
|
|
20
|
+
def self.code?(reason)
|
|
21
|
+
ALL.include?(reason.to_s.strip)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -102,9 +102,16 @@ module PermissionForge
|
|
|
102
102
|
end
|
|
103
103
|
|
|
104
104
|
# permissions.invalidated (the `cache` group): a user's cached hash
|
|
105
|
-
# was removed, for which tenant,
|
|
105
|
+
# was removed, for which tenant, whether the store confirmed it, and
|
|
106
|
+
# the scope of the store (Cache::Report: `process_local`, and
|
|
107
|
+
# `in_request` — which processes kept their entries when the store
|
|
108
|
+
# lives in each process).
|
|
106
109
|
def invalidated(user, tenant_id:, confirmed:)
|
|
107
|
-
|
|
110
|
+
return unless Audit.instrument?('permissions.invalidated')
|
|
111
|
+
|
|
112
|
+
scope = PermissionForge::Cache::Report.new
|
|
113
|
+
cache_event('permissions.invalidated', user, 'tenant_id' => tenant_id, 'confirmed' => confirmed,
|
|
114
|
+
'process_local' => scope.process_local?, 'in_request' => scope.in_request?)
|
|
108
115
|
end
|
|
109
116
|
|
|
110
117
|
# Whether this is the first time in this request that `key` was
|
|
@@ -27,12 +27,12 @@ module PermissionForge
|
|
|
27
27
|
# @return [AuditEvent, nil] the new event; nil when nothing changed,
|
|
28
28
|
# the table is missing, auditing is off or recording failed
|
|
29
29
|
def call(configuration: PermissionForge.configuration)
|
|
30
|
-
return unless PermissionForge::
|
|
30
|
+
return unless PermissionForge::DatabaseReach.table_exists?(PermissionForge::AuditEvent)
|
|
31
31
|
|
|
32
32
|
settings = fingerprint(configuration)
|
|
33
33
|
return if last_observed == settings
|
|
34
34
|
|
|
35
|
-
payload = { data: { 'settings' => settings }, actor: { type: 'system', label:
|
|
35
|
+
payload = { data: { 'settings' => settings }, actor: { type: 'system', label: ActorCodes::BOOT }, source: 'service' }
|
|
36
36
|
return PermissionForge::Audit.record(EVENT, **payload) unless configuration.audit_events == false
|
|
37
37
|
|
|
38
38
|
# Auditing is off: Audit.record would store nothing, yet "was auditing
|