open-loam 0.1.0 → 0.2.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 +48 -8
- data/LICENSE +1 -1
- data/README.md +24 -24
- 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/{loam → open_loam}/event_redelivery_sweep_job.rb +4 -4
- data/app/jobs/{loam → open_loam}/webhook_delivery_job.rb +7 -7
- data/app/models/{loam → open_loam}/api_token.rb +7 -7
- 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/{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 +23 -23
- 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/{loam → open_loam}/sso_provider.rb +8 -8
- data/app/models/open_loam/tenant.rb +20 -0
- data/app/models/{loam → open_loam}/translation.rb +5 -5
- data/app/models/{loam → open_loam}/webhook_endpoint.rb +6 -6
- 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 +2 -2
- data/lib/generators/{loam → open_loam}/entity/entity_generator.rb +9 -9
- data/lib/generators/{loam → open_loam}/entity/templates/api_controller.rb +1 -1
- data/lib/generators/{loam → open_loam}/entity/templates/controller.rb +21 -21
- 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 +4 -4
- 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 +90 -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 +36 -0
- data/lib/generators/{loam → open_loam}/install/install_generator.rb +47 -47
- data/lib/generators/open_loam/install/templates/AGENTS.md +341 -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/{loam → open_loam}/install/templates/admin/api_tokens_controller.rb +1 -1
- data/lib/generators/{loam → open_loam}/install/templates/admin/base_controller.rb +38 -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 +10 -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 +5 -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/{loam → open_loam}/install/templates/admin/history_controller.rb +7 -7
- data/lib/generators/{loam → open_loam}/install/templates/admin/history_index.html.erb +1 -1
- data/lib/generators/{loam → open_loam}/install/templates/admin/imports_controller.rb +12 -12
- data/lib/generators/{loam → open_loam}/install/templates/admin/inbound_webhook_sources_controller.rb +6 -6
- data/lib/generators/{loam → open_loam}/install/templates/admin/inbound_webhook_sources_new.html.erb +2 -2
- 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 +6 -6
- data/lib/generators/{loam → open_loam}/install/templates/admin/notifications_controller.rb +3 -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 +7 -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 +19 -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 +5 -5
- data/lib/generators/{loam → open_loam}/install/templates/admin/sessions_controller.rb +32 -32
- 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/sudo_controller.rb +5 -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/{loam → open_loam}/install/templates/api_base_controller.rb +15 -15
- data/lib/generators/{loam → open_loam}/install/templates/guardrails_test.rb +17 -17
- data/lib/generators/{loam → open_loam}/install/templates/import_job.rb +7 -7
- data/lib/generators/{loam → open_loam}/install/templates/inbound_webhooks_controller.rb +4 -4
- data/lib/generators/{loam → open_loam}/install/templates/initializer.rb +74 -74
- 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/{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 -9
- 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} +12 -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 +10 -10
- 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 +23 -23
- 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 +33 -33
- data/lib/{loam → open_loam}/encryption/cipher.rb +3 -3
- data/lib/{loam → open_loam}/encryption/key_provider.rb +3 -3
- data/lib/{loam → open_loam}/encryption.rb +11 -11
- data/lib/open_loam/engine.rb +57 -0
- data/lib/{loam → open_loam}/enrichers.rb +5 -5
- data/lib/{loam → open_loam}/errors.rb +10 -10
- data/lib/{loam → open_loam}/eval.rb +2 -2
- 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 +8 -8
- 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 -12
- data/lib/{loam → open_loam}/inbound_webhooks.rb +7 -7
- data/lib/{loam → open_loam}/lifecycle.rb +25 -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 +21 -21
- data/lib/{loam → open_loam}/notifications.rb +5 -5
- data/lib/{loam → open_loam}/open_api.rb +16 -16
- 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 +6 -6
- 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 +15 -15
- 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/{loam → open_loam}/sso/http_client.rb +3 -3
- data/lib/{loam → open_loam}/sso/oidc_provider.rb +2 -2
- data/lib/{loam → open_loam}/sso.rb +17 -17
- 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 +122 -0
- data/lib/tasks/{loam.rake → open_loam.rake} +43 -43
- data/lib/tasks/{loam_eval.rake → open_loam_eval.rake} +7 -7
- data/lib/tasks/open_loam_mcp.rake +20 -0
- metadata +242 -242
- data/app/models/loam/dictionary.rb +0 -20
- data/app/models/loam/search_token.rb +0 -9
- data/app/models/loam/tenant.rb +0 -20
- 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/dashboard_controller.rb +0 -10
- 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/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/version.rb +0 -3
- data/lib/loam.rb +0 -122
- data/lib/tasks/loam_mcp.rake +0 -20
- /data/lib/generators/{loam → open_loam}/install/templates/admin/api_tokens_index.html.erb +0 -0
- /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_index.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,7 +1,7 @@
|
|
|
1
|
-
module
|
|
1
|
+
module OpenLoam
|
|
2
2
|
# Per-tenant recurring jobs. Modules self-register default schedules; an app
|
|
3
|
-
# also creates them from the admin. A runner calls
|
|
4
|
-
# periodically (wire `
|
|
3
|
+
# also creates them from the admin. A runner calls OpenLoam::Scheduler.tick
|
|
4
|
+
# periodically (wire `open_loam:scheduler:tick` to system cron — every minute) and
|
|
5
5
|
# each DUE schedule enqueues its ActiveJob.
|
|
6
6
|
#
|
|
7
7
|
# NO DOUBLE-FIRE — the whole correctness story. tick ATOMICALLY CLAIMS due
|
|
@@ -12,7 +12,7 @@ module Loam
|
|
|
12
12
|
# The mechanism is behind `claim_due`, chosen per adapter. A claim also stamps
|
|
13
13
|
# `locked_until` so a crashed worker's rows free themselves after LOCK_TTL.
|
|
14
14
|
module Scheduler
|
|
15
|
-
class UnknownJobError <
|
|
15
|
+
class UnknownJobError < OpenLoam::Error; end
|
|
16
16
|
|
|
17
17
|
class << self
|
|
18
18
|
# --- declarative registry (like broadcast_events / feature_defaults) ---
|
|
@@ -31,14 +31,14 @@ module Loam
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
# Materialize the registered TENANT-scope schedules as rows in `tenant`
|
|
34
|
-
# (idempotent — safe from on_tenant_created and `
|
|
34
|
+
# (idempotent — safe from on_tenant_created and `open_loam:sync`). System-scope
|
|
35
35
|
# schedules are created explicitly by the app (they are not per-tenant).
|
|
36
36
|
def sync_tenant(tenant)
|
|
37
|
-
|
|
37
|
+
OpenLoam.as_tenant(tenant) do
|
|
38
38
|
registered.each do |default|
|
|
39
39
|
next unless default[:scope] == "tenant"
|
|
40
40
|
|
|
41
|
-
job =
|
|
41
|
+
job = OpenLoam::ScheduledJob.find_or_initialize_by(key: default[:key])
|
|
42
42
|
job.name = default[:name]
|
|
43
43
|
job.job_class = default[:job_class]
|
|
44
44
|
job.schedule = default[:schedule]
|
|
@@ -56,7 +56,7 @@ module Loam
|
|
|
56
56
|
# failure never blocks the others (its lock is released so a later tick
|
|
57
57
|
# retries it).
|
|
58
58
|
def tick(now: Time.current)
|
|
59
|
-
|
|
59
|
+
OpenLoam::Telemetry.span("scheduler_tick") { run_tick(now) }
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
def run_tick(now)
|
|
@@ -69,7 +69,7 @@ module Loam
|
|
|
69
69
|
fired += 1
|
|
70
70
|
rescue StandardError => error
|
|
71
71
|
job.update_columns(locked_until: nil, updated_at: now)
|
|
72
|
-
logger&.error("[
|
|
72
|
+
logger&.error("[open_loam scheduler] #{job.key} failed: #{error.class}: #{error.message}")
|
|
73
73
|
end
|
|
74
74
|
end
|
|
75
75
|
fired
|
|
@@ -83,7 +83,7 @@ module Loam
|
|
|
83
83
|
|
|
84
84
|
# THE code-execution guard: a job_class must resolve to a real ActiveJob
|
|
85
85
|
# subclass AND be ALLOWLISTED — either registered here or listed in
|
|
86
|
-
#
|
|
86
|
+
# OpenLoam.schedulable_jobs. "Any ActiveJob" would let a tenant admin schedule
|
|
87
87
|
# ActiveStorage::PurgeJob, a mailer's delivery job, etc.
|
|
88
88
|
def resolve_job_class(name)
|
|
89
89
|
name = name.to_s
|
|
@@ -94,7 +94,7 @@ module Loam
|
|
|
94
94
|
end
|
|
95
95
|
|
|
96
96
|
def allowed_job_class?(name)
|
|
97
|
-
registered.any? { |default| default[:job_class] == name } ||
|
|
97
|
+
registered.any? { |default| default[:job_class] == name } || OpenLoam.schedulable_jobs.include?(name)
|
|
98
98
|
end
|
|
99
99
|
|
|
100
100
|
def resolve_job_class!(name)
|
|
@@ -113,36 +113,36 @@ module Loam
|
|
|
113
113
|
if job.scope == "system"
|
|
114
114
|
klass.perform_later
|
|
115
115
|
else
|
|
116
|
-
# Carry the tenant explicitly (ActiveJob doesn't serialize
|
|
116
|
+
# Carry the tenant explicitly (ActiveJob doesn't serialize OpenLoam::Current),
|
|
117
117
|
# and enqueue inside the tenant so any enqueue-time scoping is correct.
|
|
118
|
-
|
|
118
|
+
OpenLoam.as_tenant(job.tenant) { klass.perform_later(tenant_id: job.tenant_id) }
|
|
119
119
|
end
|
|
120
120
|
end
|
|
121
121
|
|
|
122
122
|
# Atomically claim due, unlocked schedules across ALL tenants (a blessed
|
|
123
123
|
# cross-tenant scan — the runner has no tenant context, like
|
|
124
|
-
#
|
|
124
|
+
# OpenLoam::Membership.tenants_for), stamping locked_until so a concurrent tick
|
|
125
125
|
# skips them.
|
|
126
126
|
def claim_due(now)
|
|
127
|
-
lock_until = now +
|
|
128
|
-
due =
|
|
127
|
+
lock_until = now + OpenLoam::ScheduledJob::LOCK_TTL
|
|
128
|
+
due = OpenLoam::ScheduledJob.unscoped.active
|
|
129
129
|
.where("next_run_at <= ?", now)
|
|
130
130
|
.where("locked_until IS NULL OR locked_until < ?", now)
|
|
131
131
|
|
|
132
132
|
claimed = []
|
|
133
|
-
|
|
133
|
+
OpenLoam::ScheduledJob.transaction do
|
|
134
134
|
relation = postgres? ? due.lock("FOR UPDATE SKIP LOCKED") : due
|
|
135
135
|
ids = relation.pluck(:id)
|
|
136
136
|
break if ids.empty?
|
|
137
137
|
|
|
138
|
-
|
|
139
|
-
claimed =
|
|
138
|
+
OpenLoam::ScheduledJob.unscoped.where(id: ids).update_all(locked_until: lock_until)
|
|
139
|
+
claimed = OpenLoam::ScheduledJob.unscoped.where(id: ids).to_a
|
|
140
140
|
end
|
|
141
141
|
claimed
|
|
142
142
|
end
|
|
143
143
|
|
|
144
144
|
def postgres?
|
|
145
|
-
|
|
145
|
+
OpenLoam::ScheduledJob.connection.adapter_name.to_s.downcase.include?("postgres")
|
|
146
146
|
end
|
|
147
147
|
|
|
148
148
|
def logger
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
module
|
|
1
|
+
module OpenLoam
|
|
2
2
|
module Search
|
|
3
3
|
# The driver contract. Subclass and override the class methods.
|
|
4
4
|
#
|
|
@@ -10,10 +10,10 @@ module Loam
|
|
|
10
10
|
#
|
|
11
11
|
# index(record) / remove(record)
|
|
12
12
|
# Maintain the driver's index as records are saved and destroyed. Called
|
|
13
|
-
# from
|
|
13
|
+
# from OpenLoam::Searchable's after_save / after_destroy.
|
|
14
14
|
#
|
|
15
15
|
# reindex(model)
|
|
16
|
-
# Rebuild the index for a model (used by `
|
|
16
|
+
# Rebuild the index for a model (used by `open_loam:search:reindex` and seeds).
|
|
17
17
|
#
|
|
18
18
|
# A driver that needs no index (LIKE) leaves index/remove/reindex as no-ops.
|
|
19
19
|
class Driver
|
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
module
|
|
1
|
+
module OpenLoam
|
|
2
2
|
module Search
|
|
3
3
|
# The default driver: a substring LIKE across the declared columns — exactly
|
|
4
|
-
# the original
|
|
4
|
+
# the original OpenLoam::Searchable behavior, now behind the seam. Portable
|
|
5
5
|
# (Arel's `matches` emits ILIKE on PostgreSQL, case-insensitive LIKE on
|
|
6
6
|
# SQLite for ASCII) and needs no index, so index/remove/reindex are no-ops.
|
|
7
7
|
#
|
|
8
8
|
# What it does NOT do — ignore accents, split on words, rank — is the reason
|
|
9
|
-
# the seam exists: swap in
|
|
9
|
+
# the seam exists: swap in OpenLoam::Search::TokenDriver (or an external engine)
|
|
10
10
|
# and every `Model.search(q)` call site keeps working unchanged.
|
|
11
11
|
class LikeDriver < Driver
|
|
12
12
|
class << self
|
|
13
13
|
def search(model, query, scope:)
|
|
14
14
|
query = query.to_s.strip
|
|
15
|
-
return scope if query.blank? || !model.
|
|
15
|
+
return scope if query.blank? || !model.open_loam_searchable?
|
|
16
16
|
|
|
17
17
|
# sanitize_sql_like escapes the LIKE wildcards so a literal % or _ in
|
|
18
18
|
# the query stays literal; the second arg to `matches` declares that
|
|
19
19
|
# escape character to the database (SQLite has none by default).
|
|
20
20
|
pattern = "%#{model.sanitize_sql_like(query)}%"
|
|
21
|
-
conditions = model.
|
|
21
|
+
conditions = model.open_loam_searchable_columns.map do |column|
|
|
22
22
|
model.arel_table[column].matches(pattern, "\\")
|
|
23
23
|
end
|
|
24
24
|
scope.where(conditions.inject(:or))
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
module
|
|
1
|
+
module OpenLoam
|
|
2
2
|
module Search
|
|
3
3
|
# A portable, word-level index (the flagship second driver). Each record's
|
|
4
|
-
# searchable text is normalized into tokens stored in
|
|
4
|
+
# searchable text is normalized into tokens stored in open_loam_search_tokens;
|
|
5
5
|
# a query is tokenized the same way and matched with AND semantics — a record
|
|
6
6
|
# must carry EVERY query token — so "excavator cat" finds "CAT 320 Excavator"
|
|
7
7
|
# regardless of word order or position. Plain SQL: works on SQLite and
|
|
@@ -9,18 +9,18 @@ module Loam
|
|
|
9
9
|
# order-independent). When an app outgrows even this, the same seam takes a
|
|
10
10
|
# Meilisearch/Elasticsearch driver with no call-site change.
|
|
11
11
|
#
|
|
12
|
-
# Tenant isolation is free:
|
|
12
|
+
# Tenant isolation is free: OpenLoam::SearchToken is a TenantRecord, so both the
|
|
13
13
|
# match subquery and every write are scoped to the current tenant.
|
|
14
14
|
class TokenDriver < Driver
|
|
15
15
|
class << self
|
|
16
16
|
def search(model, query, scope:)
|
|
17
17
|
tokens = tokenize(query)
|
|
18
|
-
return scope if tokens.empty? || !model.
|
|
18
|
+
return scope if tokens.empty? || !model.open_loam_searchable?
|
|
19
19
|
|
|
20
20
|
# AND semantics: keep only records carrying all N distinct query
|
|
21
21
|
# tokens. Under AND every hit has the full match count, so ranking is
|
|
22
22
|
# degenerate — ordering is left to the caller.
|
|
23
|
-
matches =
|
|
23
|
+
matches = OpenLoam::SearchToken
|
|
24
24
|
.where(searchable_type: type_for(model), token: tokens)
|
|
25
25
|
.group(:searchable_id)
|
|
26
26
|
.having("COUNT(DISTINCT token) = #{tokens.size}")
|
|
@@ -33,10 +33,10 @@ module Loam
|
|
|
33
33
|
# the record's tenant, so the token rows land in the right tenant.
|
|
34
34
|
def index(record)
|
|
35
35
|
model = record.class
|
|
36
|
-
return unless model.
|
|
36
|
+
return unless model.open_loam_searchable?
|
|
37
37
|
|
|
38
38
|
type = type_for(model)
|
|
39
|
-
|
|
39
|
+
OpenLoam::SearchToken.where(searchable_type: type, searchable_id: record.id).delete_all
|
|
40
40
|
|
|
41
41
|
tokens = tokenize(searchable_text(record))
|
|
42
42
|
return if tokens.empty?
|
|
@@ -44,17 +44,17 @@ module Loam
|
|
|
44
44
|
rows = tokens.map do |token|
|
|
45
45
|
{ tenant_id: record.tenant_id, searchable_type: type, searchable_id: record.id, token: token }
|
|
46
46
|
end
|
|
47
|
-
|
|
47
|
+
OpenLoam::SearchToken.insert_all(rows)
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
def remove(record)
|
|
51
|
-
|
|
51
|
+
OpenLoam::SearchToken.where(searchable_type: type_for(record.class), searchable_id: record.id).delete_all
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
# Rebuild every current-tenant record of a model — run inside
|
|
55
|
-
#
|
|
55
|
+
# OpenLoam.as_tenant per tenant (the open_loam:search:reindex task does that).
|
|
56
56
|
def reindex(model)
|
|
57
|
-
|
|
57
|
+
OpenLoam::SearchToken.where(searchable_type: type_for(model)).delete_all
|
|
58
58
|
model.find_each { |record| index(record) }
|
|
59
59
|
end
|
|
60
60
|
|
|
@@ -65,13 +65,13 @@ module Loam
|
|
|
65
65
|
# THE SAFETY BOUNDARY: an encrypted column is skipped. Its stored value
|
|
66
66
|
# is ciphertext (meaningless to tokenize), and tokenizing the PLAINTEXT
|
|
67
67
|
# into a searchable table would recreate exactly the leak
|
|
68
|
-
#
|
|
68
|
+
# OpenLoam::Encryptable closes — an encrypted field stays exact-match-only via
|
|
69
69
|
# its blind index. `searchable_by` already refuses to declare an
|
|
70
70
|
# encrypted column; this subtraction is the second line of that defense.
|
|
71
71
|
def searchable_text(record)
|
|
72
|
-
columns = record.class.
|
|
73
|
-
if record.class.respond_to?(:
|
|
74
|
-
columns -= record.class.
|
|
72
|
+
columns = record.class.open_loam_searchable_columns
|
|
73
|
+
if record.class.respond_to?(:open_loam_encrypted_attributes)
|
|
74
|
+
columns -= record.class.open_loam_encrypted_attributes
|
|
75
75
|
end
|
|
76
76
|
columns.map { |column| record.public_send(column) }.join(" ")
|
|
77
77
|
end
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
module
|
|
1
|
+
module OpenLoam
|
|
2
2
|
# The search seam. `Model.search(q)` and the `searchable_by` declaration never
|
|
3
3
|
# change; the STRATEGY behind them is a swappable driver:
|
|
4
4
|
#
|
|
5
|
-
#
|
|
5
|
+
# OpenLoam::Search.driver = OpenLoam::Search::TokenDriver # in an initializer
|
|
6
6
|
#
|
|
7
7
|
# The default is the LIKE driver — portable and zero-setup — so nothing
|
|
8
8
|
# changes out of the box. A driver turns a text query into a subset of an
|
|
9
9
|
# already-tenant-scoped relation and maintains whatever index it needs; see
|
|
10
|
-
#
|
|
11
|
-
#
|
|
10
|
+
# OpenLoam::Search::Driver for the contract. This is the same shape as
|
|
11
|
+
# OpenLoam::EventStream.broadcaster and OpenLoam::Enrichers: one seam, drivers behind
|
|
12
12
|
# it, callers untouched.
|
|
13
13
|
module Search
|
|
14
14
|
class << self
|
|
15
15
|
attr_writer :driver
|
|
16
16
|
|
|
17
17
|
def driver
|
|
18
|
-
@driver ||=
|
|
18
|
+
@driver ||= OpenLoam::Search::LikeDriver
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def search(model, query, scope:) = driver.search(model, query, scope: scope)
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
module
|
|
1
|
+
module OpenLoam
|
|
2
2
|
# Text search over an entity's own columns:
|
|
3
3
|
#
|
|
4
|
-
# class Equipment <
|
|
5
|
-
# include
|
|
4
|
+
# class Equipment < OpenLoam::TenantRecord
|
|
5
|
+
# include OpenLoam::Searchable
|
|
6
6
|
# searchable_by :name, :status
|
|
7
7
|
# end
|
|
8
8
|
#
|
|
@@ -12,19 +12,19 @@ module Loam
|
|
|
12
12
|
# to remember: the default scope is already on it, and the result composes
|
|
13
13
|
# with ordering, pagination and anything else.
|
|
14
14
|
#
|
|
15
|
-
# HOW a query matches is a swappable driver (
|
|
15
|
+
# HOW a query matches is a swappable driver (OpenLoam::Search.driver): the default
|
|
16
16
|
# LikeDriver is a substring LIKE; the TokenDriver is a portable word-level
|
|
17
17
|
# index; an external engine is a third. This concern is only the DECLARATION
|
|
18
18
|
# (`searchable_by`) and the untouched call site (`Model.search(q)`) — swapping
|
|
19
|
-
# the driver changes neither. See
|
|
19
|
+
# the driver changes neither. See OpenLoam::Search.
|
|
20
20
|
module Searchable
|
|
21
21
|
extend ActiveSupport::Concern
|
|
22
22
|
|
|
23
23
|
included do
|
|
24
|
-
class_attribute :
|
|
24
|
+
class_attribute :open_loam_searchable_columns, default: [].freeze, instance_writer: false
|
|
25
25
|
|
|
26
26
|
# Keep the active driver's index current. A no-op under the default
|
|
27
|
-
# LikeDriver; the TokenDriver maintains
|
|
27
|
+
# LikeDriver; the TokenDriver maintains open_loam_search_tokens here. Guarded so
|
|
28
28
|
# non-searchable models pay nothing, and skipped when no searchable column
|
|
29
29
|
# actually changed — so a soft-delete (which touches only deleted_at)
|
|
30
30
|
# leaves the tokens in place, and the base scope hides the row anyway.
|
|
@@ -33,8 +33,8 @@ module Loam
|
|
|
33
33
|
# exercise the `searchable_by` DSL (its encrypted-field check), with no
|
|
34
34
|
# ActiveRecord underneath — only a real model gets the callbacks.
|
|
35
35
|
if respond_to?(:after_save)
|
|
36
|
-
after_save :
|
|
37
|
-
after_destroy :
|
|
36
|
+
after_save :open_loam_update_search_index, if: :open_loam_search_index_stale?
|
|
37
|
+
after_destroy :open_loam_remove_from_search_index, if: -> { self.class.open_loam_searchable? }
|
|
38
38
|
end
|
|
39
39
|
end
|
|
40
40
|
|
|
@@ -45,24 +45,24 @@ module Loam
|
|
|
45
45
|
def searchable_by(*columns)
|
|
46
46
|
columns = columns.map(&:to_s)
|
|
47
47
|
|
|
48
|
-
# The mirror of
|
|
48
|
+
# The mirror of OpenLoam::Encryptable's check: an encrypted column is
|
|
49
49
|
# ciphertext at rest, which LIKE cannot match. Caught here when
|
|
50
50
|
# `searchable_by` is the later declaration (encrypts catches the reverse).
|
|
51
|
-
if respond_to?(:
|
|
52
|
-
conflict = columns &
|
|
51
|
+
if respond_to?(:open_loam_encrypted_attributes)
|
|
52
|
+
conflict = columns & open_loam_encrypted_attributes
|
|
53
53
|
if conflict.any?
|
|
54
|
-
raise
|
|
54
|
+
raise OpenLoam::Error,
|
|
55
55
|
"#{name}: cannot `searchable_by` encrypted field(s) #{conflict.join(', ')} — " \
|
|
56
56
|
"ciphertext cannot be LIKE-searched. Use `encrypts :field, searchable: true` " \
|
|
57
57
|
"for exact-match lookup instead."
|
|
58
58
|
end
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
-
self.
|
|
61
|
+
self.open_loam_searchable_columns = columns.freeze
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
-
def
|
|
65
|
-
|
|
64
|
+
def open_loam_searchable?
|
|
65
|
+
open_loam_searchable_columns.any?
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
# Delegates to the active driver, handing it the current relation as the
|
|
@@ -70,21 +70,21 @@ module Loam
|
|
|
70
70
|
# (a class method called on a relation runs with `all` == that relation).
|
|
71
71
|
# A blank query returns everything: an empty search box means "no filter".
|
|
72
72
|
def search(query)
|
|
73
|
-
|
|
73
|
+
OpenLoam::Search.search(self, query, scope: all)
|
|
74
74
|
end
|
|
75
75
|
end
|
|
76
76
|
|
|
77
77
|
private
|
|
78
78
|
|
|
79
|
-
def
|
|
80
|
-
def
|
|
79
|
+
def open_loam_update_search_index = OpenLoam::Search.index(self)
|
|
80
|
+
def open_loam_remove_from_search_index = OpenLoam::Search.remove(self)
|
|
81
81
|
|
|
82
82
|
# On create every column counts as changed, so a new record is always
|
|
83
83
|
# indexed; on update, reindex only when a searchable column actually moved.
|
|
84
|
-
def
|
|
85
|
-
return false unless self.class.
|
|
84
|
+
def open_loam_search_index_stale?
|
|
85
|
+
return false unless self.class.open_loam_searchable?
|
|
86
86
|
|
|
87
|
-
saved_changes.keys.intersect?(self.class.
|
|
87
|
+
saved_changes.keys.intersect?(self.class.open_loam_searchable_columns)
|
|
88
88
|
end
|
|
89
89
|
end
|
|
90
90
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
module
|
|
1
|
+
module OpenLoam
|
|
2
2
|
# Soft-delete: destroying a business record hides it instead of erasing it.
|
|
3
3
|
# A recycle bin — for GDPR retention, for undo, for audits that still read
|
|
4
|
-
# cleanly after the fact.
|
|
4
|
+
# cleanly after the fact. OpenLoam is "safe by default", so a soft-deleted record
|
|
5
5
|
# is EXCLUDED from every ordinary query; seeing it is something you opt INTO,
|
|
6
6
|
# never a filter you must remember to add.
|
|
7
7
|
#
|
|
@@ -34,19 +34,19 @@ module Loam
|
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
def soft_delete
|
|
37
|
-
|
|
37
|
+
open_loam_toggle_deleted(deleted: true, bang: false)
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
def soft_delete!
|
|
41
|
-
|
|
41
|
+
open_loam_toggle_deleted(deleted: true, bang: true)
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
def restore
|
|
45
|
-
|
|
45
|
+
open_loam_toggle_deleted(deleted: false, bang: false)
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
def restore!
|
|
49
|
-
|
|
49
|
+
open_loam_toggle_deleted(deleted: false, bang: true)
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
private
|
|
@@ -55,18 +55,18 @@ module Loam
|
|
|
55
55
|
# record to the state it is already in (a double-clicked button) is a no-op,
|
|
56
56
|
# so the trail never fills with deleted_at -> deleted_at noise.
|
|
57
57
|
#
|
|
58
|
-
# A soft-delete is an UPDATE, so
|
|
58
|
+
# A soft-delete is an UPDATE, so OpenLoam::Auditable's after_destroy never fires
|
|
59
59
|
# for it. Rather than write our own audit row we borrow Auditable's single
|
|
60
|
-
# audit path via `
|
|
60
|
+
# audit path via `open_loam_audit_as`, which relabels this save's audit
|
|
61
61
|
# "soft_delete"/"restore" instead of the "update" it would otherwise record.
|
|
62
|
-
def
|
|
62
|
+
def open_loam_toggle_deleted(deleted:, bang:)
|
|
63
63
|
return true if deleted == deleted?
|
|
64
64
|
|
|
65
65
|
self.deleted_at = deleted ? Time.current : nil
|
|
66
66
|
action = deleted ? "soft_delete" : "restore"
|
|
67
67
|
write = -> { bang ? save! : save }
|
|
68
68
|
|
|
69
|
-
respond_to?(:
|
|
69
|
+
respond_to?(:open_loam_audit_as, true) ? open_loam_audit_as(action, &write) : write.call
|
|
70
70
|
end
|
|
71
71
|
end
|
|
72
72
|
end
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
require "net/http"
|
|
2
2
|
require "json"
|
|
3
3
|
|
|
4
|
-
module
|
|
4
|
+
module OpenLoam
|
|
5
5
|
module Sso
|
|
6
6
|
# The one place SSO talks to the network — kept tiny and isolated so the rest
|
|
7
|
-
# of the flow is pure. Only
|
|
7
|
+
# of the flow is pure. Only OpenLoam::Sso::OidcProvider uses it, and the test
|
|
8
8
|
# suite injects a FakeProvider instead, so this is never exercised offline.
|
|
9
9
|
module HttpClient
|
|
10
10
|
module_function
|
|
@@ -32,7 +32,7 @@ module Loam
|
|
|
32
32
|
|
|
33
33
|
def parse(response)
|
|
34
34
|
unless response.is_a?(Net::HTTPSuccess)
|
|
35
|
-
raise
|
|
35
|
+
raise OpenLoam::Sso::Error, "identity provider returned #{response.code}"
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
JSON.parse(response.body)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
module
|
|
1
|
+
module OpenLoam
|
|
2
2
|
module Sso
|
|
3
3
|
# The real OIDC Authorization Code provider. Discovers the issuer's endpoints
|
|
4
4
|
# from its .well-known document, sends the user to the authorization endpoint,
|
|
@@ -13,7 +13,7 @@ module Loam
|
|
|
13
13
|
# JWKS verification for the prototype. Roadmap: full id_token + JWKS.
|
|
14
14
|
#
|
|
15
15
|
# NOT exercised by the test suite (a FakeProvider is injected via
|
|
16
|
-
#
|
|
16
|
+
# OpenLoam::Sso.builder); live OIDC against a real IdP is verified manually.
|
|
17
17
|
class OidcProvider
|
|
18
18
|
SCOPE = "openid email profile".freeze
|
|
19
19
|
|
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
require "
|
|
2
|
-
require "
|
|
3
|
-
require "
|
|
4
|
-
require "
|
|
1
|
+
require "open_loam/sso/claims"
|
|
2
|
+
require "open_loam/sso/http_client"
|
|
3
|
+
require "open_loam/sso/oidc_provider"
|
|
4
|
+
require "open_loam/sso/fake_provider"
|
|
5
5
|
|
|
6
|
-
module
|
|
6
|
+
module OpenLoam
|
|
7
7
|
# Single sign-on. SHIPPED: OIDC Authorization Code flow end-to-end —
|
|
8
8
|
# home-realm discovery by email domain, just-in-time user provisioning, IdP
|
|
9
9
|
# group -> role mapping, and account linking to an existing User. SEAMS
|
|
10
10
|
# (documented, not built): SAML (another protocol behind the same interface,
|
|
11
11
|
# raises NotImplementedError until implemented) and SCIM 2.0 provisioning
|
|
12
|
-
# (see docs/_foundation/overview.md). See
|
|
12
|
+
# (see docs/_foundation/overview.md). See OpenLoam::SsoProvider for the per-tenant config.
|
|
13
13
|
#
|
|
14
14
|
# A protocol provider implements two methods:
|
|
15
15
|
# authorization_url(state:, login_hint: nil) -> the IdP URL to redirect to
|
|
16
|
-
# exchange(code:) ->
|
|
16
|
+
# exchange(code:) -> OpenLoam::Sso::Claims
|
|
17
17
|
#
|
|
18
18
|
# The provider is built through a swappable `builder` so tests and the demo
|
|
19
|
-
# inject a FakeProvider and NOTHING touches the network.
|
|
19
|
+
# inject a FakeProvider and NOTHING touches the network. OpenLoam::Sso::OidcProvider
|
|
20
20
|
# is the real one; it is never constructed in the test suite.
|
|
21
21
|
module Sso
|
|
22
|
-
class Error <
|
|
22
|
+
class Error < OpenLoam::Error; end
|
|
23
23
|
|
|
24
24
|
# The identity provider did not assert a verified email. Linking or creating
|
|
25
25
|
# an account on an unverified email is an account-takeover vector, so the
|
|
@@ -39,7 +39,7 @@ module Loam
|
|
|
39
39
|
@builder ||= method(:default_builder)
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
-
# Wrap a
|
|
42
|
+
# Wrap a OpenLoam::SsoProvider record in its protocol provider.
|
|
43
43
|
def build(record, redirect_uri:)
|
|
44
44
|
builder.call(record, redirect_uri)
|
|
45
45
|
end
|
|
@@ -50,25 +50,25 @@ module Loam
|
|
|
50
50
|
OidcProvider.new(record, redirect_uri: redirect_uri)
|
|
51
51
|
when "saml"
|
|
52
52
|
raise NotImplementedError,
|
|
53
|
-
"SAML SSO is a documented seam — implement
|
|
53
|
+
"SAML SSO is a documented seam — implement OpenLoam::Sso::SamlProvider behind this interface."
|
|
54
54
|
else
|
|
55
55
|
raise Error, "Unsupported SSO protocol: #{record.protocol.inspect}"
|
|
56
56
|
end
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
# Home-realm discovery. THE blessed cross-tenant lookup for SSO (like
|
|
60
|
-
#
|
|
60
|
+
# OpenLoam::Membership.tenants_for): "which tenant's IdP owns this email
|
|
61
61
|
# domain?" is asked at the sign-in page, before any tenant is chosen, so it
|
|
62
62
|
# reaches across tenants via `unscoped` — something host code must never do.
|
|
63
63
|
def provider_for(email:)
|
|
64
64
|
domain = email.to_s.split("@").last.to_s.strip.downcase
|
|
65
65
|
return nil if domain.blank?
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
OpenLoam::SsoProvider.unscoped.where(domain: domain, active: true).first
|
|
68
68
|
end
|
|
69
69
|
|
|
70
70
|
# Resolve verified IdP claims to a signed-in User, in the provider's tenant.
|
|
71
|
-
# MUST be called inside
|
|
71
|
+
# MUST be called inside OpenLoam.as_tenant(provider.tenant). Order matters:
|
|
72
72
|
# match the durable (provider, sub) identity first, then an existing User by
|
|
73
73
|
# verified email (link), else just-in-time create — always with a tenant
|
|
74
74
|
# membership at the mapped role.
|
|
@@ -86,13 +86,13 @@ module Loam
|
|
|
86
86
|
|
|
87
87
|
# Resolve the user: the durable (provider, sub) identity first, then an
|
|
88
88
|
# existing User by verified email (link), else just-in-time create.
|
|
89
|
-
identity =
|
|
89
|
+
identity = OpenLoam::SsoIdentity.find_by(sso_provider_id: provider.id, sub: claims.sub)
|
|
90
90
|
user = identity&.user || User.find_by(email: claims.email) || jit_create_user(claims)
|
|
91
91
|
|
|
92
92
|
# Re-map claims -> role on EVERY login (including a returning identity),
|
|
93
93
|
# so an IdP role change takes effect.
|
|
94
94
|
ensure_membership(provider, user, claims)
|
|
95
|
-
|
|
95
|
+
OpenLoam::SsoIdentity.create!(user: user, sso_provider: provider, sub: claims.sub) unless identity
|
|
96
96
|
user
|
|
97
97
|
end
|
|
98
98
|
|
|
@@ -120,7 +120,7 @@ module Loam
|
|
|
120
120
|
# (a promotion reflects, a removed group downgrades) rather than freezing
|
|
121
121
|
# at whatever the role was when the membership was first created.
|
|
122
122
|
def ensure_membership(provider, user, claims)
|
|
123
|
-
membership =
|
|
123
|
+
membership = OpenLoam::Membership.find_or_initialize_by(user_id: user.id)
|
|
124
124
|
membership.role = role_for(provider, claims)
|
|
125
125
|
membership.save!
|
|
126
126
|
end
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
module
|
|
1
|
+
module OpenLoam
|
|
2
2
|
# A dependency-free observability seam (L-712). Wrap a unit of work in a span;
|
|
3
3
|
# by default it emits an `ActiveSupport::Notifications` event
|
|
4
|
-
# ("
|
|
4
|
+
# ("open_loam.span.<name>") carrying the attributes and duration, so ANY subscriber
|
|
5
5
|
# — an OpenTelemetry bridge, StatsD, a log line — picks it up with no hard
|
|
6
|
-
# dependency on
|
|
6
|
+
# dependency on OpenLoam's side. An app that wants real OTLP spans replaces the
|
|
7
7
|
# whole strategy:
|
|
8
8
|
#
|
|
9
|
-
#
|
|
9
|
+
# OpenLoam::Telemetry.backend = ->(name, attributes, work) do
|
|
10
10
|
# Tracer.in_span(name, attributes: attributes) { work.call }
|
|
11
11
|
# end
|
|
12
12
|
#
|
|
13
|
-
#
|
|
13
|
+
# OpenLoam instruments its async/background hot paths (scheduler tick, durable
|
|
14
14
|
# event delivery, inbound webhook ingest) so an operator sees them without
|
|
15
15
|
# wiring anything per-call.
|
|
16
16
|
module Telemetry
|
|
@@ -24,7 +24,7 @@ module Loam
|
|
|
24
24
|
if backend
|
|
25
25
|
backend.call(name.to_s, attributes, block)
|
|
26
26
|
else
|
|
27
|
-
ActiveSupport::Notifications.instrument("
|
|
27
|
+
ActiveSupport::Notifications.instrument("open_loam.span.#{name}", **attributes, &block)
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
|