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
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
module Api
|
|
2
|
+
# Base class for every JSON endpoint. Authentication is a bearer token:
|
|
3
|
+
#
|
|
4
|
+
# curl -H "Authorization: Bearer <token>" http://localhost:3000/api/equipment
|
|
5
|
+
#
|
|
6
|
+
# A token identifies one user in one tenant, and `OpenLoam::ApiToken.authenticate`
|
|
7
|
+
# establishes both in OpenLoam::Current before any action runs. From that point
|
|
8
|
+
# on this is ordinary tenant-scoped OpenLoam code: every query is filtered, every
|
|
9
|
+
# write is audited, every policy applies — the API is not a side door.
|
|
10
|
+
#
|
|
11
|
+
# Finding a token is the one lookup that cannot start from a tenant (the
|
|
12
|
+
# token is what reveals the tenant), which is why it lives in the gem rather
|
|
13
|
+
# than here: host app code must never reach across tenants.
|
|
14
|
+
class BaseController < ActionController::API
|
|
15
|
+
before_action :authenticate_api_token!
|
|
16
|
+
|
|
17
|
+
# Same guard as the admin base controller: an endpoint that never authorized
|
|
18
|
+
# anything fails loudly rather than silently serving. Runs after the action,
|
|
19
|
+
# so it catches the omission in tests, not at runtime.
|
|
20
|
+
after_action :verify_authorized!
|
|
21
|
+
|
|
22
|
+
def self.skip_authorization!(reason, **options)
|
|
23
|
+
raise ArgumentError, "skip_authorization! needs a reason" if reason.to_s.strip.empty?
|
|
24
|
+
|
|
25
|
+
skip_after_action :verify_authorized!, **options
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
rescue_from OpenLoam::NotAuthorizedError do
|
|
29
|
+
render json: { error: "forbidden" }, status: :forbidden
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
rescue_from ActiveRecord::RecordNotFound do
|
|
33
|
+
render json: { error: "not_found" }, status: :not_found
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
rescue_from OpenLoam::UnknownCustomFieldError do |error|
|
|
37
|
+
render json: { error: error.message }, status: :unprocessable_entity
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def authenticate_api_token!
|
|
43
|
+
@api_token = OpenLoam::ApiToken.authenticate(bearer_token)
|
|
44
|
+
|
|
45
|
+
render json: { error: "unauthorized" }, status: :unauthorized unless @api_token
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def bearer_token
|
|
49
|
+
request.headers["Authorization"].to_s[/\ABearer (.+)\z/, 1]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def current_tenant = OpenLoam::Current.tenant
|
|
53
|
+
def current_actor = OpenLoam::Current.actor
|
|
54
|
+
|
|
55
|
+
def policy_for(record)
|
|
56
|
+
OpenLoam::Policy.for(record)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def authorize!(policy, action)
|
|
60
|
+
authorized!
|
|
61
|
+
raise OpenLoam::NotAuthorizedError unless policy.public_send(action)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Marked whether the answer was yes or no — a refusal is still a check.
|
|
65
|
+
def authorized!
|
|
66
|
+
@open_loam_authorized = true
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def verify_authorized!
|
|
70
|
+
return if @open_loam_authorized
|
|
71
|
+
|
|
72
|
+
raise OpenLoam::AuthorizationNotPerformedError,
|
|
73
|
+
"#{self.class.name}##{action_name} finished without authorizing. Call authorize!, or declare " \
|
|
74
|
+
"`skip_authorization! \"<why>\"` if it is authorized structurally."
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# The JSON shape of an entity: the columns this actor's role may READ, plus
|
|
78
|
+
# the readable custom fields (which live in the `custom_fields` column that
|
|
79
|
+
# every generated entity carries).
|
|
80
|
+
#
|
|
81
|
+
# The read check is the same one the CSV export and the admin screens apply.
|
|
82
|
+
# Without it the API is a way around every `field readable:` rule in the app:
|
|
83
|
+
# the record is already loaded and `record.attributes` emits everything on it.
|
|
84
|
+
#
|
|
85
|
+
# Encrypted fields (OpenLoam::Encryptable) are returned DECRYPTED — the caller is
|
|
86
|
+
# authenticated, tenant-scoped and policy-gated, exactly like the admin show
|
|
87
|
+
# screen — and their blind-index `<field>_hash` column is dropped, so the
|
|
88
|
+
# equality-leaking hash never goes over the wire.
|
|
89
|
+
def entity_json(record, enrichments: nil)
|
|
90
|
+
policy = policy_for(record)
|
|
91
|
+
json = record.attributes.slice(*policy.readable_fields(record.attribute_names))
|
|
92
|
+
|
|
93
|
+
if record.class.respond_to?(:open_loam_encrypted_attributes)
|
|
94
|
+
record.class.open_loam_encrypted_attributes.each do |name|
|
|
95
|
+
json[name] = record.public_send(name) if json.key?(name)
|
|
96
|
+
json.delete("#{name}_hash")
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# `custom_fields` is one JSON column, so it is all-or-nothing at the
|
|
101
|
+
# column level — filter its contents per definition instead.
|
|
102
|
+
if json.key?("custom_fields") && record.class.respond_to?(:custom_field_definitions)
|
|
103
|
+
readable = record.class.custom_field_definitions.map(&:name).select { |n| policy.custom_field_readable?(n) }
|
|
104
|
+
json["custom_fields"] = json["custom_fields"].to_h.slice(*readable)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Computed cross-module blocks (OpenLoam::Enrichers), under a separate key so
|
|
108
|
+
# they're never confused with the record's own columns. An index passes the
|
|
109
|
+
# batched result (avoiding N+1); a single show computes it here.
|
|
110
|
+
enrichments = OpenLoam::Enrichers.enrich(record) if enrichments.nil?
|
|
111
|
+
json["enrichments"] = enrichments if enrichments.present?
|
|
112
|
+
json
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Same field-level enforcement as the admin screens: only a definition the
|
|
116
|
+
# actor's role may write is assigned. Custom fields ride at the top level
|
|
117
|
+
# of the request body, beside the entity key.
|
|
118
|
+
def assign_custom_fields!(record, params, policy)
|
|
119
|
+
submitted = params[:custom_fields]
|
|
120
|
+
return unless submitted
|
|
121
|
+
|
|
122
|
+
submitted.each do |name, value|
|
|
123
|
+
record.set_custom_field(name, value) if policy.custom_field_writable?(name)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
require "test_helper"
|
|
2
2
|
|
|
3
|
-
# Structural guardrails. These are not feature tests — they enforce
|
|
3
|
+
# Structural guardrails. These are not feature tests — they enforce OpenLoam's
|
|
4
4
|
# invariants on the codebase itself. If one of these fails, an agent (or a
|
|
5
5
|
# human) stepped outside the conventions.
|
|
6
|
-
class
|
|
7
|
-
# Models that are legitimately not tenant-scoped.
|
|
6
|
+
class OpenLoamGuardrailsTest < ActiveSupport::TestCase
|
|
7
|
+
# Models that are legitimately not tenant-scoped. OpenLoam::Config holds both
|
|
8
8
|
# global (tenant_id NULL) and per-tenant override rows, so tenancy is a
|
|
9
|
-
# nullable column and resolution lives in vetted gem code (
|
|
10
|
-
#
|
|
9
|
+
# nullable column and resolution lives in vetted gem code (OpenLoam::Configs).
|
|
10
|
+
# OpenLoam::MfaCredential belongs to the person, who spans tenants — MFA is
|
|
11
11
|
# verified at login before any tenant is chosen.
|
|
12
|
-
TENANCY_ALLOWLIST = %w[ApplicationRecord User
|
|
12
|
+
TENANCY_ALLOWLIST = %w[ApplicationRecord User OpenLoam::Tenant OpenLoam::Config OpenLoam::MfaCredential OpenLoam::AuthAttempt].freeze
|
|
13
13
|
|
|
14
14
|
# Rails' own engine models (storage, rich text, jobs) are framework
|
|
15
15
|
# plumbing, not business data — they are out of scope for tenant linting.
|
|
16
16
|
FRAMEWORK_NAMESPACES = %w[ActiveStorage:: ActionText:: ActionMailbox:: SolidQueue:: SolidCache:: SolidCable::].freeze
|
|
17
17
|
|
|
18
|
-
test "every app model is tenant-scoped (inherits
|
|
18
|
+
test "every app model is tenant-scoped (inherits OpenLoam::TenantRecord)" do
|
|
19
19
|
Rails.application.eager_load!
|
|
20
20
|
|
|
21
21
|
offenders = ActiveRecord::Base.descendants.reject do |model|
|
|
@@ -23,20 +23,20 @@ class LoamGuardrailsTest < ActiveSupport::TestCase
|
|
|
23
23
|
model.name.nil? ||
|
|
24
24
|
TENANCY_ALLOWLIST.include?(model.name) ||
|
|
25
25
|
FRAMEWORK_NAMESPACES.any? { |ns| model.name.start_with?(ns) } ||
|
|
26
|
-
model <=
|
|
26
|
+
model <= OpenLoam::TenantRecord
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
assert_empty offenders,
|
|
30
30
|
"These models are NOT tenant-scoped: #{offenders.map(&:name).join(', ')}. " \
|
|
31
|
-
"Business models must inherit
|
|
31
|
+
"Business models must inherit OpenLoam::TenantRecord (use `rails g open_loam:entity`). " \
|
|
32
32
|
"If a model is intentionally global, add it to TENANCY_ALLOWLIST here — in review, on purpose."
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
test "touching a tenant-scoped model with no tenant context raises" do
|
|
36
|
-
|
|
36
|
+
OpenLoam::Current.reset
|
|
37
37
|
|
|
38
|
-
assert_raises(
|
|
39
|
-
assert_raises(
|
|
38
|
+
assert_raises(OpenLoam::MissingTenantError) { OpenLoam::AuditRecord.count }
|
|
39
|
+
assert_raises(OpenLoam::MissingTenantError) { OpenLoam::Membership.first }
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
test "nobody uses .unscoped outside vetted framework code" do
|
|
@@ -64,16 +64,16 @@ class LoamGuardrailsTest < ActiveSupport::TestCase
|
|
|
64
64
|
"docs/ and link it, rather than raising the budget."
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
-
test "every
|
|
67
|
+
test "every OpenLoam::FieldDefinition entity_type resolves to a class that uses OpenLoam::CustomFields" do
|
|
68
68
|
Rails.application.eager_load!
|
|
69
69
|
|
|
70
|
-
offenders =
|
|
70
|
+
offenders = OpenLoam::FieldDefinition.unscoped.distinct.pluck(:entity_type).reject do |entity_type|
|
|
71
71
|
klass = entity_type.safe_constantize
|
|
72
|
-
klass && klass.include?(
|
|
72
|
+
klass && klass.include?(OpenLoam::CustomFields)
|
|
73
73
|
end
|
|
74
74
|
|
|
75
75
|
assert_empty offenders,
|
|
76
|
-
"These
|
|
77
|
-
"`include
|
|
76
|
+
"These OpenLoam::FieldDefinition entity_type values don't resolve to a class that " \
|
|
77
|
+
"`include OpenLoam::CustomFields`: #{offenders.join(', ')}. Likely a typo when the field was created."
|
|
78
78
|
end
|
|
79
79
|
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Runs a CSV import in the background, advancing a OpenLoam::ProgressJob so the admin
|
|
2
|
+
# watches it live (L-915). Carries tenant + actor explicitly, like every OpenLoam job.
|
|
3
|
+
# The per-row summary (created/updated/failed + error rows) is stored on the
|
|
4
|
+
# ProgressJob's `result` for the summary screen.
|
|
5
|
+
#
|
|
6
|
+
# The file arrives as a blob id, never as a job argument. ActiveJob arguments are
|
|
7
|
+
# serialized into the queue backend and echoed in the job log, so passing the CSV
|
|
8
|
+
# itself would persist every row — including columns bound for encrypted fields —
|
|
9
|
+
# in the clear, outside the encryption the destination columns exist to provide.
|
|
10
|
+
class ImportJob < ApplicationJob
|
|
11
|
+
queue_as :default
|
|
12
|
+
|
|
13
|
+
def perform(entity_type:, blob_id:, mapping:, match_key:, tenant_id:, actor_id:)
|
|
14
|
+
tenant = OpenLoam::Tenant.find(tenant_id)
|
|
15
|
+
actor = User.find_by(id: actor_id)
|
|
16
|
+
model = OpenLoam::Import.allowed_model(entity_type)
|
|
17
|
+
blob = ActiveStorage::Blob.find_by(id: blob_id)
|
|
18
|
+
return if blob.nil? # already purged: nothing to import, not an error
|
|
19
|
+
|
|
20
|
+
csv = blob.download.force_encoding("UTF-8")
|
|
21
|
+
|
|
22
|
+
OpenLoam.as_tenant(tenant, actor: actor) do
|
|
23
|
+
total = [ CSV.parse(csv).size - 1, 0 ].max
|
|
24
|
+
progress = OpenLoam::Progress.start(name: "Import #{entity_type}", total: total)
|
|
25
|
+
begin
|
|
26
|
+
result = OpenLoam::Import.run(csv, model: model, mapping: mapping, actor: actor, match_key: match_key, progress: progress)
|
|
27
|
+
progress.update_columns(result: result.to_h)
|
|
28
|
+
progress.complete!
|
|
29
|
+
rescue StandardError => error
|
|
30
|
+
progress.fail!(error.message)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
ensure
|
|
34
|
+
# The upload has done its job; leaving it behind is a second copy of the
|
|
35
|
+
# same data with none of the destination's protections.
|
|
36
|
+
blob&.purge_later
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
# Public endpoint that receives webhooks FROM external systems at
|
|
2
2
|
# /webhooks/:token. No session, no bearer token: the HMAC signature is the auth
|
|
3
3
|
# (ActionController::API, so there is no CSRF to skip). All the verified,
|
|
4
|
-
# replay-resistant work lives in
|
|
4
|
+
# replay-resistant work lives in OpenLoam::InboundWebhooks.ingest; this controller
|
|
5
5
|
# only maps its Result to an HTTP status and never leaks tenant context to the
|
|
6
6
|
# next request on this thread.
|
|
7
7
|
class InboundWebhooksController < ActionController::API
|
|
8
8
|
def receive
|
|
9
|
-
result =
|
|
9
|
+
result = OpenLoam::InboundWebhooks.ingest(
|
|
10
10
|
token: params[:token], raw_body: request.raw_post, headers: request.headers
|
|
11
11
|
)
|
|
12
|
-
Rails.logger.info("[
|
|
12
|
+
Rails.logger.info("[open_loam inbound] #{result.status} #{result.reason}") if result.status >= 400
|
|
13
13
|
head result.status
|
|
14
14
|
ensure
|
|
15
|
-
|
|
15
|
+
OpenLoam::Current.reset
|
|
16
16
|
end
|
|
17
17
|
end
|
|
@@ -1,214 +1,226 @@
|
|
|
1
|
-
#
|
|
1
|
+
# OpenLoam configuration, tenant lifecycle hooks, and domain event subscriptions.
|
|
2
2
|
|
|
3
|
-
# Master key for field encryption at rest (
|
|
3
|
+
# Master key for field encryption at rest (OpenLoam::Encryptable). Per-tenant keys
|
|
4
4
|
# are derived from it via HKDF, so it must be STABLE and SECRET — never commit it.
|
|
5
|
-
# Set
|
|
5
|
+
# Set OPEN_LOAM_MASTER_KEY in the environment (or wire Rails.application.credentials)
|
|
6
6
|
# to a high-entropy value, e.g. `SecureRandom.hex(32)`. Only apps that actually
|
|
7
7
|
# `encrypts` a field need it; encryption raises a clear error if it is missing.
|
|
8
8
|
#
|
|
9
|
-
#
|
|
9
|
+
# OpenLoam::Encryption.master_key = ENV.fetch("OPEN_LOAM_MASTER_KEY")
|
|
10
10
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
11
|
+
# To ROTATE: keep the outgoing key in OPEN_LOAM_PREVIOUS_MASTER_KEY while the new
|
|
12
|
+
# one is in OPEN_LOAM_MASTER_KEY. Reads fall back to the old key, writes always use
|
|
13
|
+
# the new one, so `bin/rails open_loam:encryption:rotate[Model,tenant_id]` can walk
|
|
14
|
+
# each tenant's rows. Drop the previous key once every model has been rotated —
|
|
15
|
+
# until then, both keys are live and the old one is still worth protecting.
|
|
13
16
|
|
|
14
17
|
# Roles every tenant of this app is expected to have. A registry read by your
|
|
15
|
-
# own seeding/admin code —
|
|
18
|
+
# own seeding/admin code — OpenLoam does not create memberships for you, because
|
|
16
19
|
# who gets which role is business logic.
|
|
17
|
-
|
|
20
|
+
OpenLoam.default_roles = %w[manager employee]
|
|
18
21
|
|
|
19
|
-
# Locales that CONTENT translations (
|
|
22
|
+
# Locales that CONTENT translations (OpenLoam::Translatable) may be authored in — the
|
|
20
23
|
# admin's language switcher and the per-record Translations screen offer these.
|
|
21
24
|
# The first is the base/default; the rest overlay onto it. This is for user data
|
|
22
|
-
# (a product name), NOT developer UI strings — those stay Rails i18n.
|
|
23
|
-
# a `
|
|
24
|
-
# I18n.locale too, so add `config/locales/
|
|
25
|
+
# (a product name), NOT developer UI strings — those stay Rails i18n. OpenLoam ships
|
|
26
|
+
# a `open_loam.*` base locale (English) for its own chrome; the admin switcher sets
|
|
27
|
+
# I18n.locale too, so add `config/locales/open_loam.pl.yml` (etc.) to translate the UI
|
|
25
28
|
# for each locale you list here.
|
|
26
|
-
|
|
29
|
+
OpenLoam.locales = %w[en]
|
|
27
30
|
|
|
28
|
-
# Observability (
|
|
31
|
+
# Observability (OpenLoam::Telemetry). By default OpenLoam wraps its async hot paths
|
|
29
32
|
# (scheduler tick, durable event delivery, inbound webhook ingest) in spans that
|
|
30
|
-
# emit ActiveSupport::Notifications events ("
|
|
33
|
+
# emit ActiveSupport::Notifications events ("open_loam.span.*") — subscribe to those,
|
|
31
34
|
# or plug a real tracer in here:
|
|
32
35
|
#
|
|
33
|
-
#
|
|
34
|
-
# OpenTelemetry.tracer_provider.tracer("
|
|
36
|
+
# OpenLoam::Telemetry.backend = ->(name, attributes, work) do
|
|
37
|
+
# OpenTelemetry.tracer_provider.tracer("open_loam").in_span(name, attributes: attributes.transform_keys(&:to_s)) { work.call }
|
|
35
38
|
# end
|
|
36
39
|
|
|
37
|
-
# Feature-string permissions (
|
|
40
|
+
# Feature-string permissions (OpenLoam::Permissions) — a finer capability layer under
|
|
38
41
|
# the coarse role. Deny-by-default; `*` grants everything, a trailing `.*` is a
|
|
39
|
-
# prefix ("equipment.*"). Check with `
|
|
42
|
+
# prefix ("equipment.*"). Check with `OpenLoam.can?("equipment.edit")`,
|
|
40
43
|
# `require_permission!("...")` in a controller, or the `can?` view helper.
|
|
41
|
-
# Orthogonal to roles (
|
|
44
|
+
# Orthogonal to roles (OpenLoam::Membership) and field-level policies (OpenLoam::Policy).
|
|
42
45
|
#
|
|
43
|
-
#
|
|
46
|
+
# OpenLoam::Permissions.configure do
|
|
44
47
|
# role :admin, allow: "*"
|
|
45
48
|
# role :manager, allow: %w[equipment.* billing.read]
|
|
46
49
|
# role :clerk, allow: %w[equipment.read]
|
|
47
50
|
# end
|
|
48
51
|
|
|
49
|
-
# Customization WITHOUT forking (
|
|
50
|
-
# one of
|
|
52
|
+
# Customization WITHOUT forking (OpenLoam::Overrides): disable or replace an entry in
|
|
53
|
+
# one of OpenLoam's keyed registries (:widgets, :broadcast_events). A stale override
|
|
51
54
|
# (a key that no longer exists) is warned about at boot by `check!`, so a
|
|
52
55
|
# typo'd override is visible instead of silently doing nothing.
|
|
53
56
|
#
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
+
# OpenLoam::Overrides.disable(:widgets, "open_progress") # drop a built-in widget
|
|
58
|
+
# OpenLoam::Overrides.replace(:widgets, "audit_recent") { |actor| { kind: "count", value: 0 } }
|
|
59
|
+
# OpenLoam::Overrides.disable(:broadcast_events, "open_loam.progress.") # stop pushing it over SSE
|
|
57
60
|
#
|
|
58
|
-
# BOUNDARY: this is only for
|
|
61
|
+
# BOUNDARY: this is only for OpenLoam's in-gem registries. Override a VIEW, CONTROLLER,
|
|
59
62
|
# or ROUTE the standard Rails way (create a file at the same path to shadow the
|
|
60
63
|
# engine's, or `prepend` a module) — not here.
|
|
61
64
|
|
|
62
|
-
# Events pushed live to the browser over SSE (
|
|
65
|
+
# Events pushed live to the browser over SSE (OpenLoam::EventStream). Default off; the
|
|
63
66
|
# notification pattern is enabled here so the admin bell increments without a
|
|
64
67
|
# reload. Add patterns (e.g. "billing.") to stream domain events to live widgets —
|
|
65
68
|
# nothing reaches the browser unless it matches a pattern here (security posture).
|
|
66
|
-
|
|
69
|
+
OpenLoam.broadcast_events = [ "open_loam.notification.", "open_loam.progress." ]
|
|
67
70
|
|
|
68
|
-
#
|
|
71
|
+
# Events EXCLUDED from the event log (OpenLoam::EventLog), which otherwise
|
|
72
|
+
# captures everything. Progress ticks are excluded because a bulk import fires
|
|
73
|
+
# one per row — volume without history worth keeping.
|
|
74
|
+
OpenLoam.uncaptured_events = [ "open_loam.progress." ]
|
|
75
|
+
|
|
76
|
+
# How long captured events are kept before the daily per-tenant prune. nil keeps
|
|
77
|
+
# everything, which means an unbounded table.
|
|
78
|
+
OpenLoam.event_log_retention = 90.days
|
|
79
|
+
|
|
80
|
+
# Response enrichers (OpenLoam::Enrichers): attach a computed block onto ANOTHER
|
|
69
81
|
# module's entity in admin/API responses, with no foreign-key coupling — billing
|
|
70
82
|
# can annotate an Equipment without Equipment knowing billing exists. Pass a
|
|
71
83
|
# `batch:` resolver (array -> { id => value }) so an index stays one query, not N.
|
|
72
84
|
#
|
|
73
|
-
#
|
|
85
|
+
# OpenLoam::Enrichers.register("Equipment", key: "outstanding_balance", batch: ->(equipments) do
|
|
74
86
|
# totals = Invoice.where(equipment_id: equipments.map(&:id)).group(:equipment_id).sum(:balance)
|
|
75
87
|
# equipments.map(&:id).index_with { |id| totals.fetch(id, 0) }
|
|
76
88
|
# end)
|
|
77
89
|
|
|
78
|
-
# Search backend (
|
|
90
|
+
# Search backend (OpenLoam::Search). The default is a substring LIKE — portable and
|
|
79
91
|
# zero-setup. Opt into the word-level TokenDriver (also portable, no external
|
|
80
|
-
# service: it keeps a normalized-token index in
|
|
92
|
+
# service: it keeps a normalized-token index in open_loam_search_tokens) for
|
|
81
93
|
# order-independent, whole-word matching. `searchable_by` and every
|
|
82
94
|
# `Model.search(q)` call site are UNCHANGED — only this line differs; an external
|
|
83
95
|
# engine (Meilisearch/Elasticsearch) is a third driver behind the same seam.
|
|
84
|
-
# After switching, backfill existing rows once with `bin/rails
|
|
96
|
+
# After switching, backfill existing rows once with `bin/rails open_loam:search:reindex`
|
|
85
97
|
# (new and updated records index themselves on save).
|
|
86
98
|
#
|
|
87
|
-
#
|
|
99
|
+
# OpenLoam::Search.driver = OpenLoam::Search::TokenDriver
|
|
88
100
|
|
|
89
|
-
# Custom-field read-model index (
|
|
90
|
-
# custom field is index-backed (a typed projection in
|
|
101
|
+
# Custom-field read-model index (OpenLoam::CustomFieldIndex). Filtering/sorting on a
|
|
102
|
+
# custom field is index-backed (a typed projection in open_loam_custom_field_values)
|
|
91
103
|
# instead of a per-row JSON scan. Records project themselves on save; backfill
|
|
92
|
-
# existing data once with `bin/rails
|
|
93
|
-
# `
|
|
94
|
-
# `
|
|
104
|
+
# existing data once with `bin/rails open_loam:index:reindex` (or
|
|
105
|
+
# `OpenLoam::CustomFieldIndex.reindex(Model)`). Query with
|
|
106
|
+
# `OpenLoam::CustomFieldIndex.filter(Model, field_key, op, value)` — the generated
|
|
95
107
|
# entity index routes a `cf_field`/`cf_op`/`cf_value` filter through it.
|
|
96
108
|
|
|
97
|
-
# Single sign-on (
|
|
109
|
+
# Single sign-on (OpenLoam::Sso). OIDC is shipped end-to-end: configure a per-tenant
|
|
98
110
|
# provider under /admin/sso_providers (issuer, client_id, client_secret, email
|
|
99
111
|
# domain, default JIT role). Home-realm discovery routes a user to their tenant's
|
|
100
112
|
# IdP by email domain; a verified identity is JIT-provisioned or linked to an
|
|
101
|
-
# existing User. The client_secret is encrypted at rest, so
|
|
102
|
-
# be set (same key as the rest of
|
|
113
|
+
# existing User. The client_secret is encrypted at rest, so OPEN_LOAM_MASTER_KEY must
|
|
114
|
+
# be set (same key as the rest of OpenLoam::Encryptable). SAML and SCIM are documented
|
|
103
115
|
# seams (see docs/_foundation/overview.md). Nothing to register here for real OIDC.
|
|
104
116
|
#
|
|
105
117
|
# In tests, inject the offline fake so the flow never hits the network:
|
|
106
118
|
#
|
|
107
|
-
#
|
|
119
|
+
# OpenLoam::Sso.builder = ->(record, redirect_uri) { OpenLoam::Sso::FakeProvider.new(record, redirect_uri: redirect_uri) }
|
|
108
120
|
|
|
109
|
-
# Roles that MUST use two-factor auth (
|
|
121
|
+
# Roles that MUST use two-factor auth (OpenLoam::MfaCredential). At login, a user
|
|
110
122
|
# whose role in the chosen tenant is on this list and who has not enrolled is
|
|
111
|
-
# sent to set MFA up before they can work. Resolved via
|
|
123
|
+
# sent to set MFA up before they can work. Resolved via OpenLoam::Configs, so it can
|
|
112
124
|
# be a global default or a per-tenant override; empty means MFA is optional.
|
|
113
125
|
#
|
|
114
|
-
#
|
|
126
|
+
# OpenLoam::Configs.set("security.mfa_required_roles", ["manager"], scope: :global)
|
|
115
127
|
#
|
|
116
|
-
# Auth rate-limiting / lockout (
|
|
128
|
+
# Auth rate-limiting / lockout (OpenLoam::AuthThrottle) guards failed password/TOTP/
|
|
117
129
|
# sudo attempts. Defaults: lock after 10 failures within a 15-minute window.
|
|
118
|
-
# Tune via
|
|
130
|
+
# Tune via OpenLoam::Configs (global or per-tenant):
|
|
119
131
|
#
|
|
120
|
-
#
|
|
121
|
-
#
|
|
132
|
+
# OpenLoam::Configs.set("security.max_auth_attempts", 5, scope: :global)
|
|
133
|
+
# OpenLoam::Configs.set("security.auth_window_minutes", 10, scope: :global)
|
|
122
134
|
#
|
|
123
135
|
# Rack::Attack (with a cache store) is the production-scale path across multiple
|
|
124
136
|
# processes; the built-in DB counter is the portable single-process default.
|
|
125
137
|
|
|
126
|
-
# App-wide setting defaults (
|
|
138
|
+
# App-wide setting defaults (OpenLoam::Configs). A key resolves override → global
|
|
127
139
|
# row → this declared default, so declaring one here needs no migration and no
|
|
128
140
|
# row — a tenant reads the default until someone overrides it in the admin
|
|
129
141
|
# Settings screen (/admin/configs). Values keep their type (bool/number/string/
|
|
130
142
|
# hash), and a per-tenant override never leaks to another tenant.
|
|
131
143
|
#
|
|
132
|
-
#
|
|
144
|
+
# OpenLoam.config_defaults = { "billing.currency" => "USD", "billing.net_terms" => 30 }
|
|
133
145
|
#
|
|
134
146
|
# To change the company-wide baseline at runtime (not per tenant), write a
|
|
135
|
-
# global row: `
|
|
147
|
+
# global row: `OpenLoam::Configs.set("billing.currency", "EUR", scope: :global)`.
|
|
136
148
|
|
|
137
|
-
# Feature flags (
|
|
149
|
+
# Feature flags (OpenLoam::Features). Declare known capabilities and their default
|
|
138
150
|
# state; a flag with no row resolves to its declared default, and managers flip
|
|
139
151
|
# a tenant's override at /admin/features. A flag gates a CAPABILITY (is this on
|
|
140
152
|
# for the tenant), orthogonal to roles/policies, which gate a PERSON.
|
|
141
153
|
#
|
|
142
|
-
#
|
|
154
|
+
# OpenLoam.feature_defaults = {
|
|
143
155
|
# "beta_dashboard" => { default: false, description: "New dashboard, rolled out per tenant." }
|
|
144
156
|
# }
|
|
145
157
|
#
|
|
146
158
|
# In a controller: `require_feature!(:beta_dashboard)` (404s when off); in a
|
|
147
159
|
# view: `<%% if feature_on?(:beta_dashboard) %>`. Flip app-wide with
|
|
148
|
-
# `
|
|
160
|
+
# `OpenLoam::Features.enable(:beta_dashboard, scope: :global)`.
|
|
149
161
|
|
|
150
162
|
# What a brand-new tenant gets for free. Registered at file scope on purpose:
|
|
151
163
|
# inside `to_prepare` this would re-register on every code reload.
|
|
152
|
-
|
|
164
|
+
OpenLoam.on_tenant_created do |tenant|
|
|
153
165
|
# Seed roles/defaults for a new tenant here. MUST be idempotent —
|
|
154
|
-
# `bin/rails
|
|
166
|
+
# `bin/rails open_loam:sync` re-runs these for every existing tenant, which is how
|
|
155
167
|
# a default added in a later release reaches tenants that already exist.
|
|
156
168
|
# Use find_or_create_by!, never create!. The block runs inside
|
|
157
|
-
#
|
|
169
|
+
# OpenLoam.as_tenant(tenant), so tenant-scoped writes need no extra ceremony.
|
|
158
170
|
#
|
|
159
|
-
# Baseline managed lookup lists (
|
|
171
|
+
# Baseline managed lookup lists (OpenLoam::Dictionary) — a FieldDefinition of type
|
|
160
172
|
# "dictionary" can then point a custom field at the key, rendering a select:
|
|
161
173
|
#
|
|
162
|
-
# severity =
|
|
174
|
+
# severity = OpenLoam::Dictionary.find_or_create_by!(key: "damage_severity") { |d| d.name = "Damage severity" }
|
|
163
175
|
# [%w[minor Minor], %w[major Major], %w[critical Critical]].each_with_index do |(value, label), i|
|
|
164
176
|
# severity.entries.find_or_create_by!(value: value) { |e| e.label = label; e.position = i }
|
|
165
177
|
# end
|
|
166
178
|
|
|
167
|
-
# Materialize the registered tenant-scope schedules (see
|
|
179
|
+
# Materialize the registered tenant-scope schedules (see OpenLoam::Scheduler.register
|
|
168
180
|
# below) as rows for this tenant. A no-op until you register one.
|
|
169
|
-
|
|
181
|
+
OpenLoam::Scheduler.sync_tenant(tenant)
|
|
170
182
|
end
|
|
171
183
|
|
|
172
|
-
# Recurring jobs (
|
|
184
|
+
# Recurring jobs (OpenLoam::Scheduler). Register default schedules here at file scope;
|
|
173
185
|
# on_tenant_created materializes the tenant-scope ones per tenant. A "tenant" job
|
|
174
|
-
# is enqueued with `tenant_id:` (establish it with
|
|
186
|
+
# is enqueued with `tenant_id:` (establish it with OpenLoam.as_tenant); a "system" job
|
|
175
187
|
# runs once with no tenant. job_class MUST be a real ActiveJob (validated — never
|
|
176
188
|
# arbitrary code). Wire the runner to system cron, every minute:
|
|
177
189
|
#
|
|
178
|
-
# * * * * * cd /path/to/app && bin/rails
|
|
190
|
+
# * * * * * cd /path/to/app && bin/rails open_loam:scheduler:tick
|
|
179
191
|
#
|
|
180
192
|
# The claim is atomic, so running it from several hosts never double-fires a job.
|
|
181
193
|
#
|
|
182
|
-
#
|
|
194
|
+
# OpenLoam::Scheduler.register(key: "nightly_digest", name: "Nightly digest",
|
|
183
195
|
# job_class: "DigestJob", schedule: "0 7 * * *", scope: "tenant")
|
|
184
196
|
#
|
|
185
197
|
# Only ALLOWLISTED job classes are schedulable (a registered one, or one listed
|
|
186
198
|
# here) — so a tenant admin can't schedule ActiveStorage::PurgeJob or a mailer:
|
|
187
199
|
#
|
|
188
|
-
#
|
|
200
|
+
# OpenLoam.schedulable_jobs = %w[DigestJob ReindexJob]
|
|
189
201
|
|
|
190
202
|
# Domain event subscriptions. Subscribe to a single event or a whole domain
|
|
191
203
|
# (trailing dot = prefix). Register them here at file scope, not inside
|
|
192
204
|
# `to_prepare`: subscriptions are global, so a reload would add a second copy
|
|
193
205
|
# of each one and every event would be handled twice.
|
|
194
206
|
#
|
|
195
|
-
#
|
|
207
|
+
# OpenLoam::Events.subscribe("billing.subscription.renewed") do |name, payload|
|
|
196
208
|
# BillingMailer.renewal_receipt(payload[:id]).deliver_later
|
|
197
209
|
# end
|
|
198
210
|
#
|
|
199
|
-
#
|
|
200
|
-
# Rails.logger.info("[
|
|
211
|
+
# OpenLoam::Events.subscribe("rental.") do |name, payload|
|
|
212
|
+
# Rails.logger.info("[open_loam event] #{name} #{payload.inspect}")
|
|
201
213
|
# end
|
|
202
214
|
#
|
|
203
215
|
# Event -> in-app notification is the intended pattern for telling someone
|
|
204
216
|
# something. The subscriber runs in the publisher's tenant context, so the
|
|
205
217
|
# notification lands in the right tenant with no extra ceremony:
|
|
206
218
|
#
|
|
207
|
-
#
|
|
208
|
-
#
|
|
219
|
+
# OpenLoam::Events.subscribe("rental.damage_report.approve") do |_name, payload|
|
|
220
|
+
# OpenLoam::Notifications.notify_role(:manager, title: "Damage report approved")
|
|
209
221
|
# end
|
|
210
222
|
|
|
211
|
-
# Business rules (
|
|
223
|
+
# Business rules (OpenLoam::BusinessRule) are the admin-editable cousin of the
|
|
212
224
|
# subscriptions above: a manager declares, per tenant, WHEN a condition holds on
|
|
213
225
|
# a triggering record THEN run a fixed vocabulary of safe actions (notify,
|
|
214
226
|
# emit_event, set_field, block_transition). The condition is DATA — a
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
class CreateOpenLoamApiTokens < ActiveRecord::Migration[<%= ActiveRecord::VERSION::STRING.to_f %>]
|
|
2
|
+
def change
|
|
3
|
+
create_table :open_loam_api_tokens<%= open_loam_id_option %> do |t|
|
|
4
|
+
t.references :tenant, null: false, foreign_key: { to_table: :open_loam_tenants }<%= open_loam_type_option %>
|
|
5
|
+
t.references :user, null: false, foreign_key: true<%= open_loam_type_option %>
|
|
6
|
+
t.string :token_digest, null: false # SHA-256; the plaintext is never stored
|
|
7
|
+
t.string :label
|
|
8
|
+
t.datetime :last_used_at
|
|
9
|
+
t.timestamps
|
|
10
|
+
end
|
|
11
|
+
# Tokens are looked up before a tenant is known, so the uniqueness that
|
|
12
|
+
# matters is global.
|
|
13
|
+
add_index :open_loam_api_tokens, :token_digest, unique: true
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class CreateOpenLoamAuditRecords < ActiveRecord::Migration[<%= ActiveRecord::VERSION::STRING.to_f %>]
|
|
2
|
+
def change
|
|
3
|
+
create_table :open_loam_audit_records<%= open_loam_id_option %> do |t|
|
|
4
|
+
t.references :tenant, null: false, foreign_key: { to_table: :open_loam_tenants }<%= open_loam_type_option %>
|
|
5
|
+
t.string :auditable_type, null: false
|
|
6
|
+
t.<%= open_loam_key_column_type %> :auditable_id<%= open_loam_key_limit_option %>, null: false
|
|
7
|
+
t.string :action, null: false
|
|
8
|
+
t.<%= open_loam_key_column_type %> :actor_id<%= open_loam_key_limit_option %>
|
|
9
|
+
t.text :changeset
|
|
10
|
+
t.timestamps
|
|
11
|
+
end
|
|
12
|
+
add_index :open_loam_audit_records, %i[tenant_id auditable_type auditable_id]
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
class
|
|
1
|
+
class CreateOpenLoamAuthAttempts < ActiveRecord::Migration[<%= ActiveRecord::VERSION::STRING.to_f %>]
|
|
2
2
|
def change
|
|
3
|
-
create_table :
|
|
3
|
+
create_table :open_loam_auth_attempts<%= open_loam_id_option %> do |t|
|
|
4
4
|
t.string :identifier, null: false # the submitted login (normalized), NOT tenant-scoped
|
|
5
5
|
t.string :kind, null: false # password / totp / recovery / sudo
|
|
6
6
|
t.string :ip # nullable; for an optional per-ip throttle
|
|
7
7
|
t.timestamps
|
|
8
8
|
end
|
|
9
|
-
add_index :
|
|
9
|
+
add_index :open_loam_auth_attempts, %i[identifier created_at]
|
|
10
10
|
end
|
|
11
11
|
end
|