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,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
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
require "net/http"
|
|
2
|
+
require "json"
|
|
3
|
+
|
|
4
|
+
module OpenLoam
|
|
5
|
+
module Sso
|
|
6
|
+
# The one place SSO talks to the network — kept tiny and isolated so the rest
|
|
7
|
+
# of the flow is pure. Only OpenLoam::Sso::OidcProvider uses it, and the test
|
|
8
|
+
# suite injects a FakeProvider instead, so this is never exercised offline.
|
|
9
|
+
module HttpClient
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def get_json(url, bearer: nil)
|
|
13
|
+
uri, address = checked(url)
|
|
14
|
+
request = Net::HTTP::Get.new(uri)
|
|
15
|
+
request["Authorization"] = "Bearer #{bearer}" if bearer
|
|
16
|
+
parse(perform(uri, address, request))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def post_form(url, params)
|
|
20
|
+
uri, address = checked(url)
|
|
21
|
+
request = Net::HTTP::Post.new(uri)
|
|
22
|
+
request.set_form_data(params)
|
|
23
|
+
request["Accept"] = "application/json"
|
|
24
|
+
parse(perform(uri, address, request))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# The issuer is typed by a tenant manager, so these URLs are as untrusted
|
|
28
|
+
# as a webhook endpoint's. https is mandatory here rather than merely
|
|
29
|
+
# allowed: the client_secret and the code exchange travel over it.
|
|
30
|
+
def checked(url)
|
|
31
|
+
OpenLoam::OutboundUrl.resolve!(url, require_https: true)
|
|
32
|
+
rescue OpenLoam::OutboundUrl::BlockedError => error
|
|
33
|
+
raise OpenLoam::Sso::Error, "identity provider URL refused: #{error.message}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def perform(uri, address, request)
|
|
37
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: true, ipaddr: address) do |http|
|
|
38
|
+
http.request(request)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def parse(response)
|
|
43
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
44
|
+
raise OpenLoam::Sso::Error, "identity provider returned #{response.code}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
JSON.parse(response.body)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -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
|
|
@@ -32,6 +32,11 @@ module Loam
|
|
|
32
32
|
# (cross-domain account takeover), so the flow refuses it.
|
|
33
33
|
class DomainMismatchError < Error; end
|
|
34
34
|
|
|
35
|
+
# The provider has not proven it owns its `domain`, so it may not adopt an
|
|
36
|
+
# existing account. Ownership is stamped out of band by an operator
|
|
37
|
+
# (rake open_loam:sso:verify_domain).
|
|
38
|
+
class UnverifiedDomainError < Error; end
|
|
39
|
+
|
|
35
40
|
class << self
|
|
36
41
|
attr_writer :builder
|
|
37
42
|
|
|
@@ -39,7 +44,7 @@ module Loam
|
|
|
39
44
|
@builder ||= method(:default_builder)
|
|
40
45
|
end
|
|
41
46
|
|
|
42
|
-
# Wrap a
|
|
47
|
+
# Wrap a OpenLoam::SsoProvider record in its protocol provider.
|
|
43
48
|
def build(record, redirect_uri:)
|
|
44
49
|
builder.call(record, redirect_uri)
|
|
45
50
|
end
|
|
@@ -50,25 +55,28 @@ module Loam
|
|
|
50
55
|
OidcProvider.new(record, redirect_uri: redirect_uri)
|
|
51
56
|
when "saml"
|
|
52
57
|
raise NotImplementedError,
|
|
53
|
-
"SAML SSO is a documented seam — implement
|
|
58
|
+
"SAML SSO is a documented seam — implement OpenLoam::Sso::SamlProvider behind this interface."
|
|
54
59
|
else
|
|
55
60
|
raise Error, "Unsupported SSO protocol: #{record.protocol.inspect}"
|
|
56
61
|
end
|
|
57
62
|
end
|
|
58
63
|
|
|
59
64
|
# Home-realm discovery. THE blessed cross-tenant lookup for SSO (like
|
|
60
|
-
#
|
|
65
|
+
# OpenLoam::Membership.tenants_for): "which tenant's IdP owns this email
|
|
61
66
|
# domain?" is asked at the sign-in page, before any tenant is chosen, so it
|
|
62
67
|
# reaches across tenants via `unscoped` — something host code must never do.
|
|
68
|
+
#
|
|
69
|
+
# Verified providers only: the domain is manager-typed, so an unverified
|
|
70
|
+
# claim would capture every sign-in on a domain the tenant does not own.
|
|
63
71
|
def provider_for(email:)
|
|
64
72
|
domain = email.to_s.split("@").last.to_s.strip.downcase
|
|
65
73
|
return nil if domain.blank?
|
|
66
74
|
|
|
67
|
-
|
|
75
|
+
OpenLoam::SsoProvider.unscoped.domain_verified.where(domain: domain, active: true).first
|
|
68
76
|
end
|
|
69
77
|
|
|
70
78
|
# Resolve verified IdP claims to a signed-in User, in the provider's tenant.
|
|
71
|
-
# MUST be called inside
|
|
79
|
+
# MUST be called inside OpenLoam.as_tenant(provider.tenant). Order matters:
|
|
72
80
|
# match the durable (provider, sub) identity first, then an existing User by
|
|
73
81
|
# verified email (link), else just-in-time create — always with a tenant
|
|
74
82
|
# membership at the mapped role.
|
|
@@ -86,13 +94,13 @@ module Loam
|
|
|
86
94
|
|
|
87
95
|
# Resolve the user: the durable (provider, sub) identity first, then an
|
|
88
96
|
# existing User by verified email (link), else just-in-time create.
|
|
89
|
-
identity =
|
|
90
|
-
user = identity&.user ||
|
|
97
|
+
identity = OpenLoam::SsoIdentity.find_by(sso_provider_id: provider.id, sub: claims.sub)
|
|
98
|
+
user = identity&.user || link_or_create_user(provider, claims)
|
|
91
99
|
|
|
92
100
|
# Re-map claims -> role on EVERY login (including a returning identity),
|
|
93
101
|
# so an IdP role change takes effect.
|
|
94
102
|
ensure_membership(provider, user, claims)
|
|
95
|
-
|
|
103
|
+
OpenLoam::SsoIdentity.create!(user: user, sso_provider: provider, sub: claims.sub) unless identity
|
|
96
104
|
user
|
|
97
105
|
end
|
|
98
106
|
|
|
@@ -106,6 +114,19 @@ module Loam
|
|
|
106
114
|
|
|
107
115
|
private
|
|
108
116
|
|
|
117
|
+
# `User` is global, so linking adopts an account this tenant did not create
|
|
118
|
+
# — allowed only once the provider proved it owns the domain. JIT-creating
|
|
119
|
+
# needs no proof: the new user lands in the claimant's own tenant.
|
|
120
|
+
def link_or_create_user(provider, claims)
|
|
121
|
+
existing = User.find_by(email: claims.email)
|
|
122
|
+
return jit_create_user(claims) if existing.nil?
|
|
123
|
+
return existing if provider.domain_verified?
|
|
124
|
+
|
|
125
|
+
raise UnverifiedDomainError,
|
|
126
|
+
"provider for #{provider.domain.inspect} has not proven it owns that domain — " \
|
|
127
|
+
"refusing to link the existing account #{claims.email.inspect}"
|
|
128
|
+
end
|
|
129
|
+
|
|
109
130
|
def jit_create_user(claims)
|
|
110
131
|
# SSO users authenticate at the IdP; the random password satisfies
|
|
111
132
|
# has_secure_password and is unguessable, never used to log in.
|
|
@@ -120,7 +141,7 @@ module Loam
|
|
|
120
141
|
# (a promotion reflects, a removed group downgrades) rather than freezing
|
|
121
142
|
# at whatever the role was when the membership was first created.
|
|
122
143
|
def ensure_membership(provider, user, claims)
|
|
123
|
-
membership =
|
|
144
|
+
membership = OpenLoam::Membership.find_or_initialize_by(user_id: user.id)
|
|
124
145
|
membership.role = role_for(provider, claims)
|
|
125
146
|
membership.save!
|
|
126
147
|
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
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
module
|
|
1
|
+
module OpenLoam
|
|
2
2
|
# Base class for every tenant-scoped model. Inheriting from it is what makes
|
|
3
|
-
# a model a "
|
|
3
|
+
# a model a "OpenLoam entity":
|
|
4
4
|
#
|
|
5
|
-
# * every query is scoped to
|
|
5
|
+
# * every query is scoped to OpenLoam::Current.tenant via default_scope
|
|
6
6
|
# * building/creating a record assigns the current tenant automatically
|
|
7
7
|
# * touching the model with NO tenant in context raises MissingTenantError
|
|
8
8
|
#
|
|
@@ -12,24 +12,24 @@ module Loam
|
|
|
12
12
|
class TenantRecord < ActiveRecord::Base
|
|
13
13
|
self.abstract_class = true
|
|
14
14
|
|
|
15
|
-
# Generates the key when it is not an integer (see
|
|
16
|
-
include
|
|
15
|
+
# Generates the key when it is not an integer (see OpenLoam::GeneratedKey).
|
|
16
|
+
include OpenLoam::GeneratedKey
|
|
17
17
|
|
|
18
|
-
belongs_to :tenant, class_name: "
|
|
18
|
+
belongs_to :tenant, class_name: "OpenLoam::Tenant"
|
|
19
19
|
|
|
20
|
-
default_scope { where(tenant_id:
|
|
20
|
+
default_scope { where(tenant_id: OpenLoam.tenant!.id) }
|
|
21
21
|
|
|
22
22
|
validates :tenant_id, presence: true
|
|
23
23
|
|
|
24
24
|
before_validation on: :create do
|
|
25
|
-
self.tenant_id ||=
|
|
25
|
+
self.tenant_id ||= OpenLoam.tenant!.id
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
# Guardrail for writes: a record can never be saved into a foreign tenant.
|
|
29
29
|
before_save do
|
|
30
|
-
if tenant_id !=
|
|
30
|
+
if tenant_id != OpenLoam.tenant!.id
|
|
31
31
|
raise MissingTenantError, "Attempted to write #{self.class.name} into tenant #{tenant_id} " \
|
|
32
|
-
"while
|
|
32
|
+
"while OpenLoam::Current.tenant is #{OpenLoam.tenant!.id}"
|
|
33
33
|
end
|
|
34
34
|
end
|
|
35
35
|
end
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
module
|
|
1
|
+
module OpenLoam
|
|
2
2
|
# Mixed into ActiveSupport::TestCase by the install generator.
|
|
3
3
|
module TestHelpers
|
|
4
4
|
extend ActiveSupport::Concern
|
|
5
5
|
|
|
6
6
|
included do
|
|
7
|
-
setup {
|
|
8
|
-
teardown {
|
|
7
|
+
setup { OpenLoam::Current.reset }
|
|
8
|
+
teardown { OpenLoam::Current.reset }
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
def with_tenant(tenant, actor: nil, &block)
|
|
12
|
-
|
|
12
|
+
OpenLoam.as_tenant(tenant, actor: actor, &block)
|
|
13
13
|
end
|
|
14
14
|
end
|
|
15
15
|
end
|
|
@@ -3,7 +3,7 @@ require "securerandom"
|
|
|
3
3
|
require "uri"
|
|
4
4
|
require "erb"
|
|
5
5
|
|
|
6
|
-
module
|
|
6
|
+
module OpenLoam
|
|
7
7
|
# RFC 6238 time-based one-time passwords — the shipped second factor.
|
|
8
8
|
# HMAC-SHA1, 30-second step, 6 digits, ±1 step of drift tolerance (so a code
|
|
9
9
|
# entered a few seconds either side of a boundary still verifies). Hand-rolled
|
|
@@ -17,7 +17,7 @@ module Loam
|
|
|
17
17
|
# A fresh random secret, base32-encoded for authenticator apps. 20 bytes
|
|
18
18
|
# (160 bits) is the RFC-recommended size for SHA1.
|
|
19
19
|
def self.generate_secret(bytes: 20)
|
|
20
|
-
|
|
20
|
+
OpenLoam::Base32.encode(SecureRandom.random_bytes(bytes))
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
# True if `code` is valid for `secret` right now.
|
|
@@ -44,7 +44,7 @@ module Loam
|
|
|
44
44
|
|
|
45
45
|
# The HOTP code for a given step counter (RFC 4226 dynamic truncation).
|
|
46
46
|
def self.code_at(secret, counter, digits: DIGITS)
|
|
47
|
-
hmac = OpenSSL::HMAC.digest("SHA1",
|
|
47
|
+
hmac = OpenSSL::HMAC.digest("SHA1", OpenLoam::Base32.decode(secret), [counter].pack("Q>"))
|
|
48
48
|
offset = hmac.bytes.last & 0x0f
|
|
49
49
|
truncated = hmac[offset, 4].unpack1("N") & 0x7fffffff
|
|
50
50
|
(truncated % (10**digits)).to_s.rjust(digits, "0")
|
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
module
|
|
1
|
+
module OpenLoam
|
|
2
2
|
# Per-record, per-locale translations of DATA fields — a product name, a
|
|
3
3
|
# category label — distinct from Rails i18n (developer UI strings, still
|
|
4
4
|
# Rails-native and out of scope here).
|
|
5
5
|
#
|
|
6
|
-
# class Equipment <
|
|
7
|
-
# include
|
|
6
|
+
# class Equipment < OpenLoam::TenantRecord
|
|
7
|
+
# include OpenLoam::Translatable
|
|
8
8
|
# translates :name, :description
|
|
9
9
|
# end
|
|
10
10
|
#
|
|
11
11
|
# READ OVERLAY: the field reader returns the translation for the CURRENT locale
|
|
12
|
-
# (
|
|
12
|
+
# (OpenLoam.locale) when one exists, else the record's own COLUMN value — the base
|
|
13
13
|
# is the fallback and stays authoritative for the default locale:
|
|
14
14
|
#
|
|
15
|
-
# equipment.name # => overlay for
|
|
15
|
+
# equipment.name # => overlay for OpenLoam.locale, else base
|
|
16
16
|
# equipment.name(locale: "de") # => explicit locale, else base
|
|
17
17
|
# equipment.set_translation(:name, "de", "Bagger")
|
|
18
18
|
# equipment.translations_for(:name) # => { "de" => "Bagger", ... }
|
|
19
19
|
#
|
|
20
20
|
# Writes still go through the ordinary `name=` writer to the base column, so
|
|
21
|
-
# translations are purely additive rows in
|
|
21
|
+
# translations are purely additive rows in open_loam_translations — the base value
|
|
22
22
|
# is never overwritten or lost.
|
|
23
23
|
module Translatable
|
|
24
24
|
extend ActiveSupport::Concern
|
|
25
25
|
|
|
26
26
|
included do
|
|
27
|
-
class_attribute :
|
|
27
|
+
class_attribute :open_loam_translatable_attributes, default: [].freeze, instance_writer: false
|
|
28
28
|
|
|
29
|
-
has_many :
|
|
29
|
+
has_many :open_loam_translations, class_name: "OpenLoam::Translation",
|
|
30
30
|
as: :translatable, dependent: :delete_all
|
|
31
31
|
end
|
|
32
32
|
|
|
@@ -35,54 +35,54 @@ module Loam
|
|
|
35
35
|
names = names.map(&:to_s)
|
|
36
36
|
|
|
37
37
|
# An encrypted field must NOT be translatable: a translation row would
|
|
38
|
-
# store its PLAINTEXT in
|
|
39
|
-
#
|
|
40
|
-
if respond_to?(:
|
|
41
|
-
conflict = names &
|
|
38
|
+
# store its PLAINTEXT in open_loam_translations, recreating exactly the leak
|
|
39
|
+
# OpenLoam::Encryptable closes (same posture as searchable_by's refusal).
|
|
40
|
+
if respond_to?(:open_loam_encrypted_attributes)
|
|
41
|
+
conflict = names & open_loam_encrypted_attributes
|
|
42
42
|
if conflict.any?
|
|
43
|
-
raise
|
|
43
|
+
raise OpenLoam::Error,
|
|
44
44
|
"#{name}: cannot `translates` encrypted field(s) #{conflict.join(', ')} — " \
|
|
45
45
|
"a translation would store plaintext. Encrypted data is not translatable."
|
|
46
46
|
end
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
-
self.
|
|
49
|
+
self.open_loam_translatable_attributes = (open_loam_translatable_attributes + names).freeze
|
|
50
50
|
|
|
51
51
|
names.each do |field|
|
|
52
52
|
# The overlay reader. `read_attribute` (not super) is the base value,
|
|
53
53
|
# so it works regardless of AR's lazy attribute-method definition.
|
|
54
54
|
define_method(field) do |locale: nil|
|
|
55
|
-
|
|
55
|
+
open_loam_translation_value(field, locale) || read_attribute(field)
|
|
56
56
|
end
|
|
57
57
|
end
|
|
58
58
|
end
|
|
59
59
|
|
|
60
|
-
def
|
|
61
|
-
|
|
60
|
+
def open_loam_translatable?(field)
|
|
61
|
+
open_loam_translatable_attributes.include?(field.to_s)
|
|
62
62
|
end
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
# The stored translation for a field in a locale (default: the current one),
|
|
66
66
|
# or nil when there is none.
|
|
67
|
-
def
|
|
68
|
-
locale = (locale ||
|
|
67
|
+
def open_loam_translation_value(field, locale = nil)
|
|
68
|
+
locale = (locale || OpenLoam.locale).to_s
|
|
69
69
|
return nil if locale.blank?
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
open_loam_translations.detect { |t| t.field == field.to_s && t.locale == locale }&.value
|
|
72
72
|
end
|
|
73
73
|
|
|
74
74
|
def set_translation(field, locale, value)
|
|
75
|
-
raise
|
|
75
|
+
raise OpenLoam::Error, "#{field} is not translatable on #{self.class.name}" unless self.class.open_loam_translatable?(field)
|
|
76
76
|
|
|
77
|
-
row =
|
|
77
|
+
row = open_loam_translations.find_or_initialize_by(field: field.to_s, locale: locale.to_s)
|
|
78
78
|
row.value = value
|
|
79
79
|
row.save!
|
|
80
|
-
|
|
80
|
+
open_loam_translations.reset
|
|
81
81
|
value
|
|
82
82
|
end
|
|
83
83
|
|
|
84
84
|
def translations_for(field)
|
|
85
|
-
|
|
85
|
+
open_loam_translations.select { |t| t.field == field.to_s }.to_h { |t| [ t.locale, t.value ] }
|
|
86
86
|
end
|
|
87
87
|
end
|
|
88
88
|
end
|