open-loam 0.1.1 → 0.3.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 +148 -8
- data/LICENSE +1 -1
- data/README.md +27 -25
- data/app/jobs/{loam → open_loam}/custom_field_reindex_job.rb +6 -6
- data/app/jobs/{loam → open_loam}/event_delivery_job.rb +8 -8
- data/app/jobs/open_loam/event_log_prune_job.rb +15 -0
- data/app/jobs/{loam → open_loam}/event_redelivery_sweep_job.rb +4 -4
- data/app/jobs/{loam → open_loam}/webhook_delivery_job.rb +17 -8
- data/app/models/open_loam/api_token.rb +63 -0
- data/app/models/{loam → open_loam}/audit_record.rb +3 -3
- data/app/models/{loam → open_loam}/auth_attempt.rb +5 -5
- data/app/models/{loam → open_loam}/business_rule.rb +10 -10
- data/app/models/{loam → open_loam}/business_rule_run.rb +5 -5
- data/app/models/{loam → open_loam}/comment.rb +9 -9
- data/app/models/{loam → open_loam}/config.rb +7 -7
- data/app/models/{loam → open_loam}/custom_field_value.rb +5 -5
- data/app/models/{loam → open_loam}/dashboard_widget.rb +5 -5
- data/app/models/open_loam/dictionary.rb +20 -0
- data/app/models/{loam → open_loam}/dictionary_entry.rb +7 -7
- data/app/models/{loam → open_loam}/event_delivery.rb +4 -4
- data/app/models/open_loam/event_record.rb +42 -0
- data/app/models/{loam → open_loam}/field_definition.rb +8 -8
- data/app/models/{loam → open_loam}/inbound_webhook_delivery.rb +4 -4
- data/app/models/{loam → open_loam}/inbound_webhook_source.rb +13 -13
- data/app/models/{loam → open_loam}/membership.rb +6 -6
- data/app/models/{loam → open_loam}/mfa_credential.rb +8 -8
- data/app/models/{loam → open_loam}/notification.rb +10 -10
- data/app/models/{loam → open_loam}/pending_action.rb +70 -26
- data/app/models/{loam → open_loam}/perspective.rb +5 -5
- data/app/models/{loam → open_loam}/progress_job.rb +7 -7
- data/app/models/{loam → open_loam}/record_lock.rb +5 -5
- data/app/models/{loam → open_loam}/scheduled_job.rb +7 -7
- data/app/models/open_loam/search_token.rb +9 -0
- data/app/models/{loam → open_loam}/sso_identity.rb +4 -4
- data/app/models/open_loam/sso_provider.rb +66 -0
- data/app/models/open_loam/tenant.rb +20 -0
- data/app/models/{loam → open_loam}/translation.rb +5 -5
- data/app/models/open_loam/webhook_endpoint.rb +39 -0
- data/app/views/{loam → open_loam}/attachments/_attachments.html.erb +1 -1
- data/app/views/{loam → open_loam}/comments/_comments.html.erb +1 -1
- data/app/views/{loam → open_loam}/custom_fields/_fields.html.erb +6 -2
- data/lib/generators/{loam → open_loam}/entity/entity_generator.rb +9 -9
- data/lib/generators/{loam → open_loam}/entity/templates/api_controller.rb +2 -1
- data/lib/generators/{loam → open_loam}/entity/templates/controller.rb +32 -22
- data/lib/generators/{loam → open_loam}/entity/templates/entity_test.rb +12 -12
- data/lib/generators/{loam → open_loam}/entity/templates/migration.rb +5 -5
- data/lib/generators/{loam → open_loam}/entity/templates/model.rb +10 -10
- data/lib/generators/{loam → open_loam}/entity/templates/policy.rb +1 -1
- data/lib/generators/{loam → open_loam}/entity/templates/views/_form.html.erb +8 -5
- data/lib/generators/{loam → open_loam}/entity/templates/views/deleted.html.erb +7 -7
- data/lib/generators/open_loam/entity/templates/views/edit.html.erb +3 -0
- data/lib/generators/open_loam/entity/templates/views/index.html.erb +94 -0
- data/lib/generators/open_loam/entity/templates/views/new.html.erb +3 -0
- data/lib/generators/open_loam/entity/templates/views/show.html.erb +38 -0
- data/lib/generators/{loam → open_loam}/install/install_generator.rb +58 -47
- data/lib/generators/open_loam/install/templates/AGENTS.md +345 -0
- data/lib/generators/{loam → open_loam}/install/templates/admin/api_docs_controller.rb +2 -2
- data/lib/generators/{loam → open_loam}/install/templates/admin/api_docs_index.html.erb +1 -1
- data/lib/generators/open_loam/install/templates/admin/api_tokens_controller.rb +48 -0
- data/lib/generators/{loam → open_loam}/install/templates/admin/api_tokens_index.html.erb +23 -0
- data/lib/generators/{loam → open_loam}/install/templates/admin/base_controller.rb +101 -38
- data/lib/generators/{loam → open_loam}/install/templates/admin/business_rules_controller.rb +7 -7
- data/lib/generators/{loam → open_loam}/install/templates/admin/business_rules_form.html.erb +2 -2
- data/lib/generators/{loam → open_loam}/install/templates/admin/comments_controller.rb +4 -4
- data/lib/generators/{loam → open_loam}/install/templates/admin/configs_controller.rb +9 -9
- data/lib/generators/open_loam/install/templates/admin/dashboard_controller.rb +12 -0
- data/lib/generators/{loam → open_loam}/install/templates/admin/dashboard_widgets_controller.rb +5 -5
- data/lib/generators/{loam → open_loam}/install/templates/admin/dictionaries_controller.rb +5 -5
- data/lib/generators/{loam → open_loam}/install/templates/admin/dictionaries_form.html.erb +1 -1
- data/lib/generators/{loam → open_loam}/install/templates/admin/dictionary_entries_controller.rb +2 -2
- data/lib/generators/{loam → open_loam}/install/templates/admin/event_deliveries_controller.rb +5 -5
- data/lib/generators/{loam → open_loam}/install/templates/admin/event_deliveries_index.html.erb +1 -1
- data/lib/generators/{loam → open_loam}/install/templates/admin/events_controller.rb +7 -5
- data/lib/generators/{loam → open_loam}/install/templates/admin/features_controller.rb +9 -9
- data/lib/generators/{loam → open_loam}/install/templates/admin/field_definitions_controller.rb +5 -5
- data/lib/generators/{loam → open_loam}/install/templates/admin/field_definitions_new.html.erb +2 -2
- data/lib/generators/open_loam/install/templates/admin/history_controller.rb +64 -0
- data/lib/generators/{loam → open_loam}/install/templates/admin/history_index.html.erb +2 -2
- data/lib/generators/{loam → open_loam}/install/templates/admin/imports_controller.rb +25 -13
- data/lib/generators/{loam → open_loam}/install/templates/admin/inbound_webhook_sources_controller.rb +7 -7
- data/lib/generators/{loam → open_loam}/install/templates/admin/inbound_webhook_sources_new.html.erb +2 -4
- data/lib/generators/open_loam/install/templates/admin/layout.html.erb +76 -0
- data/lib/generators/{loam → open_loam}/install/templates/admin/mfa_controller.rb +11 -6
- data/lib/generators/{loam → open_loam}/install/templates/admin/notifications_controller.rb +5 -3
- data/lib/generators/{loam → open_loam}/install/templates/admin/overrides_controller.rb +3 -3
- data/lib/generators/{loam → open_loam}/install/templates/admin/overrides_index.html.erb +2 -2
- data/lib/generators/{loam → open_loam}/install/templates/admin/pending_actions_controller.rb +3 -3
- data/lib/generators/{loam → open_loam}/install/templates/admin/perspectives_controller.rb +13 -7
- data/lib/generators/{loam → open_loam}/install/templates/admin/perspectives_index.html.erb +1 -1
- data/lib/generators/open_loam/install/templates/admin/progress_jobs_controller.rb +23 -0
- data/lib/generators/{loam → open_loam}/install/templates/admin/progress_jobs_index.html.erb +1 -1
- data/lib/generators/{loam → open_loam}/install/templates/admin/record_locks_controller.rb +4 -4
- data/lib/generators/{loam → open_loam}/install/templates/admin/scheduled_jobs_controller.rb +7 -7
- data/lib/generators/{loam → open_loam}/install/templates/admin/scheduled_jobs_form.html.erb +1 -1
- data/lib/generators/{loam → open_loam}/install/templates/admin/scheduled_jobs_index.html.erb +1 -1
- data/lib/generators/{loam → open_loam}/install/templates/admin/search_controller.rb +7 -5
- data/lib/generators/{loam → open_loam}/install/templates/admin/sessions_controller.rb +33 -34
- data/lib/generators/{loam → open_loam}/install/templates/admin/sso_providers_controller.rb +6 -6
- data/lib/generators/{loam → open_loam}/install/templates/admin/sso_providers_form.html.erb +2 -2
- data/lib/generators/{loam → open_loam}/install/templates/admin/sso_providers_index.html.erb +8 -1
- data/lib/generators/{loam → open_loam}/install/templates/admin/sudo_controller.rb +7 -5
- data/lib/generators/{loam → open_loam}/install/templates/admin/sudo_new.html.erb +1 -1
- data/lib/generators/{loam → open_loam}/install/templates/admin/translations_controller.rb +7 -7
- data/lib/generators/{loam → open_loam}/install/templates/admin/translations_index.html.erb +3 -3
- data/lib/generators/{loam → open_loam}/install/templates/admin/webhook_endpoints_controller.rb +4 -4
- data/lib/generators/{loam → open_loam}/install/templates/admin/webhook_endpoints_index.html.erb +1 -1
- data/lib/generators/{loam → open_loam}/install/templates/admin.css +1 -1
- data/lib/generators/open_loam/install/templates/api_base_controller.rb +127 -0
- data/lib/generators/{loam → open_loam}/install/templates/guardrails_test.rb +17 -17
- data/lib/generators/open_loam/install/templates/import_job.rb +38 -0
- data/lib/generators/{loam → open_loam}/install/templates/inbound_webhooks_controller.rb +4 -4
- data/lib/generators/{loam → open_loam}/install/templates/initializer.rb +87 -75
- data/lib/generators/open_loam/install/templates/migrations/create_open_loam_api_tokens.rb +15 -0
- data/lib/generators/open_loam/install/templates/migrations/create_open_loam_audit_records.rb +14 -0
- data/lib/generators/{loam/install/templates/migrations/create_loam_auth_attempts.rb → open_loam/install/templates/migrations/create_open_loam_auth_attempts.rb} +3 -3
- data/lib/generators/{loam/install/templates/migrations/create_loam_business_rules.rb → open_loam/install/templates/migrations/create_open_loam_business_rules.rb} +10 -10
- data/lib/generators/open_loam/install/templates/migrations/create_open_loam_comments.rb +13 -0
- data/lib/generators/{loam/install/templates/migrations/create_loam_configs.rb → open_loam/install/templates/migrations/create_open_loam_configs.rb} +7 -7
- data/lib/generators/open_loam/install/templates/migrations/create_open_loam_custom_field_values.rb +19 -0
- data/lib/generators/open_loam/install/templates/migrations/create_open_loam_dashboard_widgets.rb +12 -0
- data/lib/generators/open_loam/install/templates/migrations/create_open_loam_dictionaries.rb +11 -0
- data/lib/generators/{loam/install/templates/migrations/create_loam_dictionary_entries.rb → open_loam/install/templates/migrations/create_open_loam_dictionary_entries.rb} +6 -6
- data/lib/generators/{loam/install/templates/migrations/create_loam_event_deliveries.rb → open_loam/install/templates/migrations/create_open_loam_event_deliveries.rb} +5 -5
- data/lib/generators/open_loam/install/templates/migrations/create_open_loam_event_records.rb +13 -0
- data/lib/generators/{loam/install/templates/migrations/create_loam_field_definitions.rb → open_loam/install/templates/migrations/create_open_loam_field_definitions.rb} +4 -4
- data/lib/generators/{loam/install/templates/migrations/create_loam_inbound_webhooks.rb → open_loam/install/templates/migrations/create_open_loam_inbound_webhooks.rb} +9 -10
- data/lib/generators/open_loam/install/templates/migrations/create_open_loam_memberships.rb +11 -0
- data/lib/generators/{loam/install/templates/migrations/create_loam_mfa_credentials.rb → open_loam/install/templates/migrations/create_open_loam_mfa_credentials.rb} +4 -4
- data/lib/generators/open_loam/install/templates/migrations/create_open_loam_notifications.rb +15 -0
- data/lib/generators/{loam/install/templates/migrations/create_loam_pending_actions.rb → open_loam/install/templates/migrations/create_open_loam_pending_actions.rb} +9 -9
- data/lib/generators/{loam/install/templates/migrations/create_loam_perspectives.rb → open_loam/install/templates/migrations/create_open_loam_perspectives.rb} +5 -5
- data/lib/generators/{loam/install/templates/migrations/create_loam_progress_jobs.rb → open_loam/install/templates/migrations/create_open_loam_progress_jobs.rb} +5 -5
- data/lib/generators/open_loam/install/templates/migrations/create_open_loam_record_locks.rb +16 -0
- data/lib/generators/{loam/install/templates/migrations/create_loam_scheduled_jobs.rb → open_loam/install/templates/migrations/create_open_loam_scheduled_jobs.rb} +5 -5
- data/lib/generators/open_loam/install/templates/migrations/create_open_loam_search_tokens.rb +18 -0
- data/lib/generators/{loam/install/templates/migrations/create_loam_sso_providers.rb → open_loam/install/templates/migrations/create_open_loam_sso_providers.rb} +15 -12
- data/lib/generators/open_loam/install/templates/migrations/create_open_loam_tenants.rb +9 -0
- data/lib/generators/open_loam/install/templates/migrations/create_open_loam_translations.rb +15 -0
- data/lib/generators/open_loam/install/templates/migrations/create_open_loam_webhook_endpoints.rb +13 -0
- data/lib/generators/{loam → open_loam}/install/templates/migrations/create_users.rb +1 -1
- data/lib/generators/{loam → open_loam}/install/templates/user.rb +5 -5
- data/lib/generators/{loam → open_loam}/primary_key_options.rb +23 -23
- data/lib/open-loam.rb +2 -2
- data/lib/{loam → open_loam}/attachable.rb +4 -4
- data/lib/{loam → open_loam}/auditable.rb +23 -23
- data/lib/{loam → open_loam}/auth_throttle.rb +20 -12
- data/lib/{loam → open_loam}/base32.rb +1 -1
- data/lib/{loam → open_loam}/bulk.rb +7 -7
- data/lib/{loam → open_loam}/business_rules/actions.rb +7 -7
- data/lib/{loam → open_loam}/business_rules/condition.rb +5 -5
- data/lib/{loam → open_loam}/business_rules.rb +19 -19
- data/lib/{loam → open_loam}/commentable.rb +6 -6
- data/lib/{loam → open_loam}/configs.rb +19 -19
- data/lib/{loam → open_loam}/cron.rb +2 -2
- data/lib/{loam → open_loam}/csv.rb +2 -2
- data/lib/{loam → open_loam}/current.rb +5 -5
- data/lib/{loam → open_loam}/custom_field_index.rb +33 -30
- data/lib/{loam → open_loam}/custom_fields.rb +17 -17
- data/lib/{loam → open_loam}/dashboard.rb +5 -5
- data/lib/{loam → open_loam}/dictionaries.rb +12 -12
- data/lib/{loam → open_loam}/durable_events.rb +19 -19
- data/lib/{loam → open_loam}/encryptable.rb +39 -34
- data/lib/{loam → open_loam}/encryption/cipher.rb +3 -3
- data/lib/{loam → open_loam}/encryption/key_provider.rb +19 -3
- data/lib/{loam → open_loam}/encryption.rb +58 -15
- data/lib/open_loam/engine.rb +65 -0
- data/lib/{loam → open_loam}/enrichers.rb +5 -5
- data/lib/{loam → open_loam}/errors.rb +20 -10
- data/lib/{loam → open_loam}/eval.rb +2 -2
- data/lib/open_loam/event_log.rb +77 -0
- data/lib/{loam → open_loam}/event_stream.rb +14 -14
- data/lib/{loam → open_loam}/eventful.rb +12 -12
- data/lib/{loam → open_loam}/events.rb +5 -5
- data/lib/{loam → open_loam}/export.rb +12 -12
- data/lib/{loam → open_loam}/features.rb +15 -15
- data/lib/{loam → open_loam}/generated_key.rb +6 -6
- data/lib/{loam → open_loam}/import.rb +12 -15
- data/lib/{loam → open_loam}/inbound_webhooks.rb +19 -18
- data/lib/{loam → open_loam}/lifecycle.rb +54 -25
- data/lib/{loam/locales/loam.en.yml → open_loam/locales/open_loam.en.yml} +5 -5
- data/lib/{loam → open_loam}/mcp/server.rb +9 -9
- data/lib/{loam → open_loam}/mcp.rb +31 -22
- data/lib/{loam → open_loam}/notifications.rb +5 -5
- data/lib/{loam → open_loam}/open_api.rb +16 -16
- data/lib/open_loam/outbound_url.rb +100 -0
- data/lib/{loam → open_loam}/overrides.rb +10 -10
- data/lib/{loam → open_loam}/pending_actions.rb +9 -9
- data/lib/{loam → open_loam}/permissions.rb +8 -8
- data/lib/{loam → open_loam}/perspectives.rb +9 -9
- data/lib/{loam → open_loam}/policy.rb +21 -8
- data/lib/{loam → open_loam}/progress.rb +6 -6
- data/lib/{loam → open_loam}/record_locks.rb +9 -9
- data/lib/{loam → open_loam}/scheduler.rb +20 -20
- data/lib/{loam → open_loam}/search/driver.rb +3 -3
- data/lib/{loam → open_loam}/search/like_driver.rb +5 -5
- data/lib/{loam → open_loam}/search/token_driver.rb +16 -16
- data/lib/{loam → open_loam}/search.rb +5 -5
- data/lib/{loam → open_loam}/searchable.rb +22 -22
- data/lib/{loam → open_loam}/soft_deletable.rb +10 -10
- data/lib/{loam → open_loam}/sso/claims.rb +1 -1
- data/lib/{loam → open_loam}/sso/fake_provider.rb +1 -1
- data/lib/open_loam/sso/http_client.rb +51 -0
- data/lib/{loam → open_loam}/sso/oidc_provider.rb +2 -2
- data/lib/{loam → open_loam}/sso.rb +39 -18
- data/lib/{loam → open_loam}/telemetry.rb +6 -6
- data/lib/{loam → open_loam}/tenant_record.rb +10 -10
- data/lib/{loam → open_loam}/test_helpers.rb +4 -4
- data/lib/{loam → open_loam}/totp.rb +3 -3
- data/lib/{loam → open_loam}/translatable.rb +24 -24
- data/lib/{loam → open_loam}/undo.rb +11 -11
- data/lib/open_loam/version.rb +3 -0
- data/lib/{loam → open_loam}/webhooks.rb +4 -4
- data/lib/{loam → open_loam}/widgets.rb +9 -9
- data/lib/{loam → open_loam}/workflow.rb +45 -45
- data/lib/open_loam.rb +124 -0
- data/lib/tasks/open_loam.rake +203 -0
- data/lib/tasks/{loam_eval.rake → open_loam_eval.rake} +7 -7
- data/lib/tasks/open_loam_mcp.rake +20 -0
- metadata +247 -242
- data/app/models/loam/api_token.rb +0 -40
- data/app/models/loam/dictionary.rb +0 -20
- data/app/models/loam/search_token.rb +0 -9
- data/app/models/loam/sso_provider.rb +0 -35
- data/app/models/loam/tenant.rb +0 -20
- data/app/models/loam/webhook_endpoint.rb +0 -26
- data/lib/generators/loam/entity/templates/views/edit.html.erb +0 -3
- data/lib/generators/loam/entity/templates/views/index.html.erb +0 -90
- data/lib/generators/loam/entity/templates/views/new.html.erb +0 -3
- data/lib/generators/loam/entity/templates/views/show.html.erb +0 -36
- data/lib/generators/loam/install/templates/AGENTS.md +0 -341
- data/lib/generators/loam/install/templates/admin/api_tokens_controller.rb +0 -30
- data/lib/generators/loam/install/templates/admin/dashboard_controller.rb +0 -10
- data/lib/generators/loam/install/templates/admin/history_controller.rb +0 -38
- data/lib/generators/loam/install/templates/admin/layout.html.erb +0 -76
- data/lib/generators/loam/install/templates/admin/progress_jobs_controller.rb +0 -19
- data/lib/generators/loam/install/templates/api_base_controller.rb +0 -90
- data/lib/generators/loam/install/templates/import_job.rb +0 -25
- data/lib/generators/loam/install/templates/migrations/create_loam_api_tokens.rb +0 -15
- data/lib/generators/loam/install/templates/migrations/create_loam_audit_records.rb +0 -14
- data/lib/generators/loam/install/templates/migrations/create_loam_comments.rb +0 -13
- data/lib/generators/loam/install/templates/migrations/create_loam_custom_field_values.rb +0 -19
- data/lib/generators/loam/install/templates/migrations/create_loam_dashboard_widgets.rb +0 -12
- data/lib/generators/loam/install/templates/migrations/create_loam_dictionaries.rb +0 -11
- data/lib/generators/loam/install/templates/migrations/create_loam_memberships.rb +0 -11
- data/lib/generators/loam/install/templates/migrations/create_loam_notifications.rb +0 -15
- data/lib/generators/loam/install/templates/migrations/create_loam_record_locks.rb +0 -16
- data/lib/generators/loam/install/templates/migrations/create_loam_search_tokens.rb +0 -18
- data/lib/generators/loam/install/templates/migrations/create_loam_tenants.rb +0 -9
- data/lib/generators/loam/install/templates/migrations/create_loam_translations.rb +0 -15
- data/lib/generators/loam/install/templates/migrations/create_loam_webhook_endpoints.rb +0 -13
- data/lib/loam/engine.rb +0 -57
- data/lib/loam/sso/http_client.rb +0 -42
- data/lib/loam/version.rb +0 -3
- data/lib/loam.rb +0 -122
- data/lib/tasks/loam.rake +0 -171
- data/lib/tasks/loam_mcp.rake +0 -20
- /data/lib/generators/{loam → open_loam}/install/templates/admin/business_rules_edit.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/business_rules_index.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/business_rules_new.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/configs_edit.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/configs_index.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/dashboard_index.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/dashboard_widgets_index.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/dictionaries_edit.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/dictionaries_index.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/dictionaries_new.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/features_index.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/field_definitions_index.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/imports_new.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/imports_preview.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/imports_summary.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/inbound_webhook_sources_index.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/mfa_activated.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/mfa_new.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/mfa_show.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/notifications_index.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/pagination.rb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/pending_actions_index.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/scheduled_jobs_edit.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/scheduled_jobs_new.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/search_index.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/sessions_mfa_challenge.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/sessions_new.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/sso_providers_edit.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/sso_providers_new.html.erb +0 -0
- /data/lib/generators/{loam → open_loam}/install/templates/admin/webhook_endpoints_new.html.erb +0 -0
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
module
|
|
2
|
-
# The real-time bridge: pushes selected
|
|
1
|
+
module OpenLoam
|
|
2
|
+
# The real-time bridge: pushes selected OpenLoam events to a browser over
|
|
3
3
|
# Server-Sent Events, so the admin updates live instead of polling.
|
|
4
4
|
#
|
|
5
5
|
# SECURITY POSTURE — default OFF. An event reaches a browser only if ALL hold:
|
|
6
|
-
# * its name matches a declared
|
|
6
|
+
# * its name matches a declared OpenLoam.broadcast_events pattern (opt-in), AND
|
|
7
7
|
# * its tenant matches the connected tenant (isolation), AND
|
|
8
8
|
# * its audience includes the connected actor (a payload `user_id`, if any,
|
|
9
9
|
# is the sole recipient; no `user_id` means tenant-wide).
|
|
10
10
|
#
|
|
11
|
-
# FAN-OUT is behind a swappable broadcaster seam (
|
|
11
|
+
# FAN-OUT is behind a swappable broadcaster seam (OpenLoam::EventStream.broadcaster).
|
|
12
12
|
# The default in-process broadcaster only sees events published in THIS process
|
|
13
13
|
# — fine for the single-process prototype; a multi-process deploy swaps in a
|
|
14
14
|
# Redis/SolidCable-backed broadcaster with no controller change (see
|
|
@@ -24,10 +24,10 @@ module Loam
|
|
|
24
24
|
# Is this event name allowed to reach browsers at all? Empty allow-list →
|
|
25
25
|
# false, always (nothing leaks by default).
|
|
26
26
|
def broadcastable?(event_name)
|
|
27
|
-
|
|
28
|
-
next false if
|
|
27
|
+
OpenLoam.broadcast_events.any? do |pattern|
|
|
28
|
+
next false if OpenLoam::Overrides.disabled?(:broadcast_events, pattern) # an app can turn a default pattern off
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
OpenLoam::Events.pattern_matches?(pattern, event_name)
|
|
31
31
|
end
|
|
32
32
|
end
|
|
33
33
|
|
|
@@ -42,9 +42,9 @@ module Loam
|
|
|
42
42
|
recipient.nil? || recipient == actor&.id
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
-
# One SSE message: an `event:` line (the
|
|
45
|
+
# One SSE message: an `event:` line (the OpenLoam event name) and a `data:`
|
|
46
46
|
# line (JSON), ended by a blank line. Only small id-ish keys ride along —
|
|
47
|
-
#
|
|
47
|
+
# OpenLoam events carry no attribute values, and this slices to a safe set as
|
|
48
48
|
# belt-and-suspenders (tenant_id is dropped; it is implied by the connection).
|
|
49
49
|
def frame(event_name, payload)
|
|
50
50
|
"event: #{event_name}\ndata: #{safe_payload(payload).to_json}\n\n"
|
|
@@ -54,21 +54,21 @@ module Loam
|
|
|
54
54
|
|
|
55
55
|
def safe_payload(payload)
|
|
56
56
|
# id-ish keys only — never attribute values. percent/status ride along for
|
|
57
|
-
# the progress bar (
|
|
57
|
+
# the progress bar (OpenLoam::ProgressJob); both are non-sensitive.
|
|
58
58
|
payload.symbolize_keys.slice(:id, :type, :user_id, :from, :to, :percent, :status).compact
|
|
59
59
|
end
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
-
# The default fan-out: subscribe to
|
|
62
|
+
# The default fan-out: subscribe to OpenLoam::Events in THIS process and forward
|
|
63
63
|
# the events deliverable to (tenant, actor) to a sink. A `sink` is anything
|
|
64
64
|
# answering #call(sse_string) — the controller's is a Queue push.
|
|
65
65
|
class InProcessBroadcaster
|
|
66
66
|
# Returns an opaque handle to pass back to #unsubscribe.
|
|
67
67
|
def subscribe(tenant:, actor:, &sink)
|
|
68
|
-
|
|
69
|
-
next unless
|
|
68
|
+
OpenLoam::Events.subscribe_all do |event_name, payload|
|
|
69
|
+
next unless OpenLoam::EventStream.deliverable?(event_name, payload, tenant: tenant, actor: actor)
|
|
70
70
|
|
|
71
|
-
sink.call(
|
|
71
|
+
sink.call(OpenLoam::EventStream.frame(event_name, payload))
|
|
72
72
|
end
|
|
73
73
|
end
|
|
74
74
|
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
module
|
|
1
|
+
module OpenLoam
|
|
2
2
|
# Lifecycle events by convention. Included in every generated entity:
|
|
3
3
|
#
|
|
4
|
-
# class Equipment <
|
|
5
|
-
# include
|
|
4
|
+
# class Equipment < OpenLoam::TenantRecord
|
|
5
|
+
# include OpenLoam::Eventful
|
|
6
6
|
# event_domain :rental # -> "rental.equipment.created" etc.
|
|
7
7
|
# end
|
|
8
8
|
#
|
|
9
|
-
# Custom business events are published explicitly with
|
|
9
|
+
# Custom business events are published explicitly with OpenLoam::Events.publish.
|
|
10
10
|
module Eventful
|
|
11
11
|
extend ActiveSupport::Concern
|
|
12
12
|
|
|
13
13
|
included do
|
|
14
|
-
class_attribute :
|
|
15
|
-
class_attribute :
|
|
14
|
+
class_attribute :open_loam_event_domain, default: "app"
|
|
15
|
+
class_attribute :open_loam_event_entity, default: nil
|
|
16
16
|
|
|
17
17
|
after_create_commit { publish_lifecycle_event("created") }
|
|
18
18
|
after_update_commit { publish_lifecycle_event("updated") }
|
|
@@ -21,23 +21,23 @@ module Loam
|
|
|
21
21
|
|
|
22
22
|
class_methods do
|
|
23
23
|
def event_domain(domain)
|
|
24
|
-
self.
|
|
24
|
+
self.open_loam_event_domain = domain.to_s
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
# The middle segment of the event name. It defaults to the model's param
|
|
28
28
|
# key, which is right for app models but awkward for namespaced ones —
|
|
29
|
-
#
|
|
30
|
-
# "
|
|
29
|
+
# OpenLoam::Comment would publish "open_loam.open_loam_comment.created" rather than
|
|
30
|
+
# "open_loam.comment.created".
|
|
31
31
|
def event_entity(name)
|
|
32
|
-
self.
|
|
32
|
+
self.open_loam_event_entity = name.to_s
|
|
33
33
|
end
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
private
|
|
37
37
|
|
|
38
38
|
def publish_lifecycle_event(happened)
|
|
39
|
-
|
|
40
|
-
"#{
|
|
39
|
+
OpenLoam::Events.publish(
|
|
40
|
+
"#{open_loam_event_domain}.#{open_loam_event_entity || model_name.param_key}.#{happened}",
|
|
41
41
|
{ id: id, type: self.class.name }
|
|
42
42
|
)
|
|
43
43
|
end
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
module
|
|
1
|
+
module OpenLoam
|
|
2
2
|
# A thin domain event bus over ActiveSupport::Notifications.
|
|
3
3
|
#
|
|
4
4
|
# Convention: event names are `domain.thing.happened`, e.g.
|
|
@@ -7,7 +7,7 @@ module Loam
|
|
|
7
7
|
# subscribers are always tenant-aware.
|
|
8
8
|
module Events
|
|
9
9
|
NAME_FORMAT = /\A[a-z0-9_]+(\.[a-z0-9_]+){2,}\z/
|
|
10
|
-
PREFIX = "
|
|
10
|
+
PREFIX = "open_loam.event."
|
|
11
11
|
|
|
12
12
|
def self.publish(name, payload = {})
|
|
13
13
|
name = name.to_s
|
|
@@ -17,12 +17,12 @@ module Loam
|
|
|
17
17
|
|
|
18
18
|
ActiveSupport::Notifications.instrument(
|
|
19
19
|
PREFIX + name,
|
|
20
|
-
payload.merge(tenant_id:
|
|
20
|
+
payload.merge(tenant_id: OpenLoam::Current.tenant&.id, actor_id: OpenLoam::Current.actor&.id)
|
|
21
21
|
)
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
# The subscription rule, in one place: a trailing dot is a domain prefix,
|
|
25
|
-
# anything else is an exact event name.
|
|
25
|
+
# anything else is an exact event name. OpenLoam::WebhookEndpoint matches
|
|
26
26
|
# against this too, so a pattern means the same thing everywhere.
|
|
27
27
|
def self.pattern_matches?(pattern, event_name)
|
|
28
28
|
pattern = pattern.to_s
|
|
@@ -31,7 +31,7 @@ module Loam
|
|
|
31
31
|
pattern.end_with?(".") ? event_name.start_with?(pattern) : event_name == pattern
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
# Every
|
|
34
|
+
# Every OpenLoam event, whatever its domain — the empty prefix matches them
|
|
35
35
|
# all. Used by the webhook dispatcher, which decides per event which
|
|
36
36
|
# endpoints care.
|
|
37
37
|
def self.subscribe_all(&block)
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
require "csv"
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module OpenLoam
|
|
4
4
|
# CSV export of a tenant-scoped relation, POLICY- and ENCRYPTION-aware:
|
|
5
5
|
#
|
|
6
|
-
#
|
|
6
|
+
# OpenLoam::Export.csv(Equipment.all, actor: current_actor)
|
|
7
7
|
#
|
|
8
|
-
# * only fields the actor's role may READ are columns (
|
|
8
|
+
# * only fields the actor's role may READ are columns (OpenLoam::Policy#readable?);
|
|
9
9
|
# * an ENCRYPTED column is NEVER exported in the clear — its cell is
|
|
10
10
|
# "[encrypted]" (the same redaction as the audit trail), so a bulk export can
|
|
11
11
|
# never become a plaintext dump of PII a role shouldn't see;
|
|
12
|
-
# * declared custom fields
|
|
13
|
-
#
|
|
12
|
+
# * declared custom fields go through the same read check
|
|
13
|
+
# (OpenLoam::Policy#custom_field_readable?) — a dictionary field exports its
|
|
14
|
+
# stored code, so the file round-trips back through OpenLoam::Import;
|
|
14
15
|
# * tenant isolation is free — the relation is already scoped.
|
|
15
16
|
#
|
|
16
17
|
# Prototype scale: builds the CSV in memory with the stdlib CSV. A very large
|
|
@@ -37,10 +38,10 @@ module Loam
|
|
|
37
38
|
# redacted), then declared custom fields.
|
|
38
39
|
def exportable_columns(model, actor)
|
|
39
40
|
policy = policy_for(model, actor)
|
|
40
|
-
encrypted = model.respond_to?(:
|
|
41
|
+
encrypted = model.respond_to?(:open_loam_encrypted_attributes) ? model.open_loam_encrypted_attributes : []
|
|
41
42
|
# The blind-index columns behind searchable encrypted fields (e.g.
|
|
42
43
|
# email_hash) are internal HMACs — never export them either.
|
|
43
|
-
blind = model.respond_to?(:
|
|
44
|
+
blind = model.respond_to?(:open_loam_searchable_encrypted_attributes) ? model.open_loam_searchable_encrypted_attributes.map { |a| "#{a}_hash" } : []
|
|
44
45
|
|
|
45
46
|
columns = model.column_names.reject { |c| SKIP_COLUMNS.include?(c) || blind.include?(c) || c == "custom_fields" }
|
|
46
47
|
.select { |c| policy.readable?(c) }
|
|
@@ -48,6 +49,8 @@ module Loam
|
|
|
48
49
|
|
|
49
50
|
if model.respond_to?(:custom_field_definitions)
|
|
50
51
|
model.custom_field_definitions.order(:name).each do |definition|
|
|
52
|
+
next unless policy.custom_field_readable?(definition.name)
|
|
53
|
+
|
|
51
54
|
columns << { header: definition.name, name: definition.name, kind: :custom }
|
|
52
55
|
end
|
|
53
56
|
end
|
|
@@ -61,14 +64,11 @@ module Loam
|
|
|
61
64
|
when :custom then (record.custom_field(column[:name]) rescue nil)
|
|
62
65
|
else record.public_send(column[:name])
|
|
63
66
|
end
|
|
64
|
-
|
|
67
|
+
OpenLoam::Csv.safe(value) # neutralize CSV formula injection (=, +, -, @, tab/CR)
|
|
65
68
|
end
|
|
66
69
|
|
|
67
70
|
def policy_for(model, actor)
|
|
68
|
-
|
|
69
|
-
# A blank instance as the record: readable? keys off the role, but the
|
|
70
|
-
# policy's custom-field checks read record.class.
|
|
71
|
-
(klass || Loam::Policy).new(actor, model.new)
|
|
71
|
+
OpenLoam::Policy.for_model(model, actor)
|
|
72
72
|
end
|
|
73
73
|
end
|
|
74
74
|
end
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
module
|
|
1
|
+
module OpenLoam
|
|
2
2
|
# Runtime feature toggles: is a capability turned ON for this tenant right now.
|
|
3
3
|
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
4
|
+
# OpenLoam::Features.on?(:beta_dashboard) # for the current tenant
|
|
5
|
+
# OpenLoam::Features.enable(:beta_dashboard) # this tenant only
|
|
6
|
+
# OpenLoam::Features.enable(:beta_dashboard, scope: :global) # everyone
|
|
7
|
+
# OpenLoam::Features.disable(:beta_dashboard)
|
|
8
|
+
# OpenLoam::Features.reset(:beta_dashboard) # drop the override → default
|
|
9
9
|
#
|
|
10
10
|
# A flag gates a CAPABILITY (is this feature live for the tenant), which is
|
|
11
11
|
# orthogonal to a policy — that gates a PERSON (may this user act). The two
|
|
12
12
|
# coexist: a manager may be allowed to approve reports AND the approvals
|
|
13
13
|
# feature may be switched off for their tenant during a rollout.
|
|
14
14
|
#
|
|
15
|
-
# This is deliberately a thin wrapper over
|
|
15
|
+
# This is deliberately a thin wrapper over OpenLoam::Configs: a flag is a boolean
|
|
16
16
|
# setting with the same override → global → declared-default resolution, so it
|
|
17
17
|
# reuses that store (no new table) and its per-request cache. Flags live under
|
|
18
18
|
# the reserved `features.` key prefix and get their own admin screen, because
|
|
@@ -22,7 +22,7 @@ module Loam
|
|
|
22
22
|
|
|
23
23
|
class << self
|
|
24
24
|
def on?(name)
|
|
25
|
-
!!
|
|
25
|
+
!!OpenLoam::Configs.get(key_for(name), default: default_for(name))
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def off?(name)
|
|
@@ -32,37 +32,37 @@ module Loam
|
|
|
32
32
|
# Flip a flag on. scope: :tenant (default) overrides for the current tenant
|
|
33
33
|
# and requires a tenant in context; scope: :global sets the app-wide state.
|
|
34
34
|
def enable(name, scope: :tenant)
|
|
35
|
-
|
|
35
|
+
OpenLoam::Configs.set(key_for(name), true, scope: scope)
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def disable(name, scope: :tenant)
|
|
39
|
-
|
|
39
|
+
OpenLoam::Configs.set(key_for(name), false, scope: scope)
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
# Drop the current tenant's override so the flag falls back to the global
|
|
43
43
|
# state / declared default.
|
|
44
44
|
def reset(name)
|
|
45
|
-
|
|
45
|
+
OpenLoam::Configs.reset(key_for(name))
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
def overridden?(name)
|
|
49
|
-
|
|
49
|
+
OpenLoam::Configs.overridden?(key_for(name))
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
# Every declared flag, sorted — the admin lists these whether or not a row
|
|
53
53
|
# exists yet, so a flag is visible the moment it is declared.
|
|
54
54
|
def declared
|
|
55
|
-
|
|
55
|
+
OpenLoam.feature_defaults.keys.sort
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
def description(name)
|
|
59
|
-
|
|
59
|
+
OpenLoam.feature_defaults.dig(name.to_s, :description)
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
# The declared-default state of a flag (false for an unknown one), used as
|
|
63
63
|
# the last resort when there is no override and no global row.
|
|
64
64
|
def default_for(name)
|
|
65
|
-
!!
|
|
65
|
+
!!OpenLoam.feature_defaults.dig(name.to_s, :default)
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
private
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
module
|
|
1
|
+
module OpenLoam
|
|
2
2
|
# Primary-key generation for tables whose key is not an integer.
|
|
3
3
|
#
|
|
4
4
|
# Rails leaves key generation to the database, which works for integer keys
|
|
@@ -7,22 +7,22 @@ module Loam
|
|
|
7
7
|
# has neither: the INSERT sends NULL and the row is rejected or lands with an
|
|
8
8
|
# empty key.
|
|
9
9
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
10
|
+
# OpenLoam's tables follow the host app's key type (see
|
|
11
|
+
# OpenLoam::Generators::PrimaryKeyOptions), so on a string-keyed app every OpenLoam
|
|
12
12
|
# model needs its key generated in Ruby. Before this, hosts patched it in
|
|
13
13
|
# themselves with a global before_create on ActiveRecord::Base — a lot to ask
|
|
14
14
|
# of an app whose only deviation was not using bigints, and a hook broad
|
|
15
|
-
# enough to reach models that had nothing to do with
|
|
15
|
+
# enough to reach models that had nothing to do with OpenLoam.
|
|
16
16
|
#
|
|
17
17
|
# Integer keys take the early return, so nothing changes for the default app.
|
|
18
18
|
module GeneratedKey
|
|
19
19
|
def self.included(base)
|
|
20
|
-
base.before_create :
|
|
20
|
+
base.before_create :assign_open_loam_generated_key
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
private
|
|
24
24
|
|
|
25
|
-
def
|
|
25
|
+
def assign_open_loam_generated_key
|
|
26
26
|
key = self.class.primary_key
|
|
27
27
|
return if key.nil? # a join table declared with id: false
|
|
28
28
|
return if self[key].present? # the caller chose the key
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
require "csv"
|
|
2
2
|
require "set"
|
|
3
3
|
|
|
4
|
-
module
|
|
4
|
+
module OpenLoam
|
|
5
5
|
# CSV import mapping engine — reusable, tenant-scoped, policy-safe.
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
7
|
+
# OpenLoam::Import.preview(csv) # headers + first rows for the mapping UI
|
|
8
|
+
# OpenLoam::Import.run(csv, model:, mapping:, actor:, # map CSV header => field
|
|
9
9
|
# match_key: "name", # update-or-create by a key (nil = create-only)
|
|
10
10
|
# dry_run: true, # validate + report, commit nothing
|
|
11
|
-
# progress: progress_job) # advance a
|
|
11
|
+
# progress: progress_job) # advance a OpenLoam::ProgressJob per row
|
|
12
12
|
#
|
|
13
13
|
# Safety: the mapping may only target fields the actor's role can WRITE (real
|
|
14
14
|
# columns + declared custom fields); tenant_id, plumbing, and non-permitted
|
|
@@ -27,14 +27,14 @@ module Loam
|
|
|
27
27
|
|
|
28
28
|
module_function
|
|
29
29
|
|
|
30
|
-
# Resolve an entity_type string to a model — ONLY a
|
|
30
|
+
# Resolve an entity_type string to a model — ONLY a OpenLoam::TenantRecord
|
|
31
31
|
# subclass (never an arbitrary constant), so an import target can't be
|
|
32
32
|
# smuggled to a global model like User (same guard as the business-rules
|
|
33
33
|
# engine).
|
|
34
34
|
def allowed_model(entity_type)
|
|
35
35
|
klass = entity_type.to_s.safe_constantize
|
|
36
|
-
unless klass.is_a?(Class) && klass <
|
|
37
|
-
raise
|
|
36
|
+
unless klass.is_a?(Class) && klass < OpenLoam::TenantRecord
|
|
37
|
+
raise OpenLoam::Error, "import target #{entity_type.inspect} is not a OpenLoam entity"
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
klass
|
|
@@ -79,7 +79,7 @@ module Loam
|
|
|
79
79
|
is_new ? (result.created += 1) : (result.updated += 1)
|
|
80
80
|
rescue StandardError => error
|
|
81
81
|
# Store ONLY the row number + message — NEVER the raw cell values. The
|
|
82
|
-
# result is persisted (
|
|
82
|
+
# result is persisted (OpenLoam::ProgressJob.result); a failed row into an
|
|
83
83
|
# encrypted field would otherwise write PLAINTEXT PII at rest.
|
|
84
84
|
result.failed += 1
|
|
85
85
|
result.errors << { "row" => line, "message" => error.message }
|
|
@@ -103,7 +103,7 @@ module Loam
|
|
|
103
103
|
out << (Array(headers) + [ "_error" ])
|
|
104
104
|
result.errors.each do |error|
|
|
105
105
|
original = data_rows[error["row"] - 2] || [] # row is 1-based incl. the header
|
|
106
|
-
out << original.map { |cell|
|
|
106
|
+
out << original.map { |cell| OpenLoam::Csv.safe(cell) } + [ error["message"] ]
|
|
107
107
|
end
|
|
108
108
|
end
|
|
109
109
|
end
|
|
@@ -115,7 +115,7 @@ module Loam
|
|
|
115
115
|
next if target.to_s.blank?
|
|
116
116
|
next if allowed.include?(target.to_s)
|
|
117
117
|
|
|
118
|
-
raise
|
|
118
|
+
raise OpenLoam::Error, "import mapping refuses #{target.inspect} — not a writable column or custom field"
|
|
119
119
|
end
|
|
120
120
|
end
|
|
121
121
|
|
|
@@ -155,14 +155,11 @@ module Loam
|
|
|
155
155
|
def safe_parse(csv_string)
|
|
156
156
|
CSV.parse(csv_string.to_s)
|
|
157
157
|
rescue CSV::MalformedCSVError => error
|
|
158
|
-
raise
|
|
158
|
+
raise OpenLoam::Error, "could not parse CSV: #{error.message}"
|
|
159
159
|
end
|
|
160
160
|
|
|
161
161
|
def policy_for(model, actor)
|
|
162
|
-
|
|
163
|
-
# A blank instance as the record: readable?/writable? key off the role, but
|
|
164
|
-
# custom_field_writable? reads record.class.custom_field_definitions.
|
|
165
|
-
(klass || Loam::Policy).new(actor, model.new)
|
|
162
|
+
OpenLoam::Policy.for_model(model, actor)
|
|
166
163
|
end
|
|
167
164
|
end
|
|
168
165
|
end
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
require "openssl"
|
|
2
2
|
require "digest"
|
|
3
3
|
|
|
4
|
-
module
|
|
4
|
+
module OpenLoam
|
|
5
5
|
# Receiving webhooks FROM external systems (the inbound sibling of
|
|
6
|
-
#
|
|
6
|
+
# OpenLoam::Webhooks). One entry point — `ingest` — does the whole verified,
|
|
7
7
|
# replay-resistant pipeline and returns a Result the controller turns into an
|
|
8
8
|
# HTTP status. Kept here (not in a controller) so it is testable without HTTP
|
|
9
9
|
# and shared by the generated app and the demo.
|
|
@@ -19,9 +19,12 @@ module Loam
|
|
|
19
19
|
# Every AUTH failure returns 401 with no distinguishing body, so a sender can't
|
|
20
20
|
# probe which check failed; the specific reason is logged server-side only.
|
|
21
21
|
#
|
|
22
|
-
# REPLAY: the real defense is the (source_id, external_id) dedupe
|
|
23
|
-
#
|
|
24
|
-
#
|
|
22
|
+
# REPLAY: the real defense is the (source_id, external_id) dedupe, and
|
|
23
|
+
# external_id is derived from the SIGNED body — never from a header. Anything
|
|
24
|
+
# an attacker can vary without invalidating the signature is a way to mint a
|
|
25
|
+
# fresh dedupe key and replay the delivery. The timestamp window is
|
|
26
|
+
# defense-in-depth only, for the same reason: unless the sender signs the
|
|
27
|
+
# timestamp, a replayer just refreshes it.
|
|
25
28
|
module InboundWebhooks
|
|
26
29
|
MAX_BYTES = 1_000_000
|
|
27
30
|
|
|
@@ -30,13 +33,13 @@ module Loam
|
|
|
30
33
|
module_function
|
|
31
34
|
|
|
32
35
|
def ingest(token:, raw_body:, headers:)
|
|
33
|
-
|
|
36
|
+
OpenLoam::Telemetry.span("inbound_webhook") { run_ingest(token, raw_body.to_s, headers) }
|
|
34
37
|
end
|
|
35
38
|
|
|
36
39
|
def run_ingest(token, raw_body, headers)
|
|
37
40
|
return Result.new(status: 413, reason: "body too large") if raw_body.bytesize > MAX_BYTES
|
|
38
41
|
|
|
39
|
-
source =
|
|
42
|
+
source = OpenLoam::InboundWebhookSource.resolve(token)
|
|
40
43
|
return Result.new(status: 404, reason: "unknown or inactive source") if source.nil?
|
|
41
44
|
|
|
42
45
|
signature = header(headers, source.signature_header_key)
|
|
@@ -51,8 +54,8 @@ module Loam
|
|
|
51
54
|
|
|
52
55
|
begin
|
|
53
56
|
delivery = nil
|
|
54
|
-
|
|
55
|
-
delivery =
|
|
57
|
+
OpenLoam::InboundWebhookDelivery.transaction do
|
|
58
|
+
delivery = OpenLoam::InboundWebhookDelivery.create!(
|
|
56
59
|
source: source, external_id: external_id, event_name: source.event_name,
|
|
57
60
|
status: "received", received_at: Time.current, payload: parse(raw_body)
|
|
58
61
|
)
|
|
@@ -60,7 +63,7 @@ module Loam
|
|
|
60
63
|
# lives on the delivery row, subscribers read it from there. Publishing
|
|
61
64
|
# inside the txn ties capture to the row — a publish failure rolls the
|
|
62
65
|
# row back so the sender's retry isn't deduped away.
|
|
63
|
-
|
|
66
|
+
OpenLoam::Events.publish(source.event_name, { source_id: source.id, delivery_id: delivery.id })
|
|
64
67
|
end
|
|
65
68
|
Result.new(status: 202, reason: "accepted", delivery: delivery)
|
|
66
69
|
rescue ActiveRecord::RecordNotUnique
|
|
@@ -92,14 +95,12 @@ module Loam
|
|
|
92
95
|
(Time.current.to_i - seconds).abs <= tolerance.to_i
|
|
93
96
|
end
|
|
94
97
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
# Consequence: identical bodies dedupe. A sender with a real delivery id
|
|
102
|
-
# should configure delivery_id_header so distinct-but-identical bodies pass.
|
|
98
|
+
# The dedupe key must come from SIGNED material, which means the body: the
|
|
99
|
+
# HMAC covers the body alone, so a captured (body, signature) replayed with a
|
|
100
|
+
# fresh delivery-id header used to mint a new external_id every time and
|
|
101
|
+
# re-publish the event. Cost: a sender emitting distinct deliveries with
|
|
102
|
+
# identical bodies sees the second deduped — its nonce belongs in the body.
|
|
103
|
+
def delivery_id(_source, _headers, body)
|
|
103
104
|
Digest::SHA256.hexdigest(body)
|
|
104
105
|
end
|
|
105
106
|
|