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,21 +1,21 @@
|
|
|
1
|
-
module
|
|
1
|
+
module OpenLoam
|
|
2
2
|
# A staged mutation awaiting human approval. When a caller runs under
|
|
3
|
-
# confirm-mode (an MCP tool acting for an AI agent — see
|
|
3
|
+
# confirm-mode (an MCP tool acting for an AI agent — see OpenLoam.mutation_mode), a
|
|
4
4
|
# write is staged HERE as a preview instead of committing; a manager approves
|
|
5
|
-
# or rejects, and only approval executes it. This is
|
|
5
|
+
# or rejects, and only approval executes it. This is OpenLoam's thesis made
|
|
6
6
|
# concrete: agent-triggered writes are gate-able.
|
|
7
7
|
#
|
|
8
|
-
# The approval gate IS a workflow (
|
|
8
|
+
# The approval gate IS a workflow (OpenLoam::Workflow): pending → (manager) approve
|
|
9
9
|
# / reject → executed / failed. "Who may approve" lives in one declarative,
|
|
10
|
-
# role-gated place. The proposed changes are encrypted at rest (
|
|
10
|
+
# role-gated place. The proposed changes are encrypted at rest (OpenLoam::Encryptable),
|
|
11
11
|
# because a staged change to an encrypted target field would otherwise sit here
|
|
12
12
|
# — and in this row's own audit — as plaintext, reopening the leak L-901 closed.
|
|
13
|
-
class PendingAction <
|
|
14
|
-
self.table_name = "
|
|
13
|
+
class PendingAction < OpenLoam::TenantRecord
|
|
14
|
+
self.table_name = "open_loam_pending_actions"
|
|
15
15
|
|
|
16
|
-
include
|
|
17
|
-
include
|
|
18
|
-
include
|
|
16
|
+
include OpenLoam::Auditable
|
|
17
|
+
include OpenLoam::Encryptable
|
|
18
|
+
include OpenLoam::Workflow
|
|
19
19
|
|
|
20
20
|
belongs_to :actor, class_name: "User", optional: true
|
|
21
21
|
belongs_to :reviewer, class_name: "User", foreign_key: :reviewed_by_id, optional: true
|
|
@@ -96,7 +96,7 @@ module Loam
|
|
|
96
96
|
# write; the record ends "executed" or "failed". Executes as `by`, so the
|
|
97
97
|
# TARGET's audit names the approving human — the person owns the change.
|
|
98
98
|
def approve!(by:)
|
|
99
|
-
|
|
99
|
+
OpenLoam.as_tenant(tenant, actor: by) do
|
|
100
100
|
reject_self_approval!(by)
|
|
101
101
|
self.reviewed_by_id = by.id
|
|
102
102
|
self.reviewed_at = Time.current
|
|
@@ -107,7 +107,7 @@ module Loam
|
|
|
107
107
|
end
|
|
108
108
|
|
|
109
109
|
def reject!(by:, reason: nil)
|
|
110
|
-
|
|
110
|
+
OpenLoam.as_tenant(tenant, actor: by) do
|
|
111
111
|
self.reviewed_by_id = by.id
|
|
112
112
|
self.reviewed_at = Time.current
|
|
113
113
|
self.result = [ "rejected", reason.presence ].compact.join(": ")
|
|
@@ -126,9 +126,9 @@ module Loam
|
|
|
126
126
|
# withdrawal, not a segregation-of-duties concern.)
|
|
127
127
|
def reject_self_approval!(by)
|
|
128
128
|
return unless by.id == actor_id
|
|
129
|
-
return if
|
|
129
|
+
return if OpenLoam::Configs.get("approvals.allow_self_approve", default: false)
|
|
130
130
|
|
|
131
|
-
raise
|
|
131
|
+
raise OpenLoam::NotAuthorizedError, "self-approval is not permitted; a different person must approve this change"
|
|
132
132
|
end
|
|
133
133
|
|
|
134
134
|
# Execution AND its status/result recording share ONE transaction with the
|
|
@@ -150,25 +150,69 @@ module Loam
|
|
|
150
150
|
update!(error: error.message)
|
|
151
151
|
end
|
|
152
152
|
|
|
153
|
+
# Never settable through a staged change: soft-delete state has its own
|
|
154
|
+
# audited action (and its own policy question), and the rest is plumbing.
|
|
155
|
+
PROTECTED_FIELDS = %w[id tenant_id deleted_at lock_version].freeze
|
|
156
|
+
|
|
153
157
|
def execute_change!
|
|
154
158
|
case action_type
|
|
155
159
|
when "create"
|
|
156
|
-
record = target_class.
|
|
160
|
+
record = target_class.new
|
|
161
|
+
authorize_action!(record, :create?)
|
|
162
|
+
record.assign_attributes(permitted_changeset(record))
|
|
163
|
+
record.save!
|
|
157
164
|
"created #{target_type}##{record.id}"
|
|
158
165
|
when "update"
|
|
159
|
-
target_class.find(target_id)
|
|
166
|
+
record = target_class.find(target_id)
|
|
167
|
+
authorize_action!(record, :update?)
|
|
168
|
+
record.update!(permitted_changeset(record))
|
|
160
169
|
"updated #{target_type}##{target_id}"
|
|
161
170
|
when "destroy"
|
|
162
171
|
target = target_class.find(target_id)
|
|
172
|
+
authorize_action!(target, :destroy?)
|
|
163
173
|
target.respond_to?(:soft_delete!) ? target.soft_delete! : target.destroy!
|
|
164
174
|
"deleted #{target_type}##{target_id}"
|
|
165
175
|
else
|
|
166
|
-
raise
|
|
176
|
+
raise OpenLoam::Error, "unknown action_type #{action_type.inspect}"
|
|
167
177
|
end
|
|
168
178
|
end
|
|
169
179
|
|
|
180
|
+
# The field rules below are not the whole policy: a host may restrict the
|
|
181
|
+
# ACTION itself (destroy? to an owner, say). Staging must not route around
|
|
182
|
+
# that, so the approver's verdict on the action is asked first.
|
|
183
|
+
def authorize_action!(record, question)
|
|
184
|
+
return if OpenLoam::Policy.for(record).public_send(question)
|
|
185
|
+
|
|
186
|
+
raise OpenLoam::NotAuthorizedError,
|
|
187
|
+
"the approver may not #{question.to_s.chomp('?')} #{target_type}"
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# target_type is a string off a stored row, so it gets the same treatment as
|
|
191
|
+
# any other type resolution from untrusted input. The TenantRecord check is
|
|
192
|
+
# the load-bearing half: a non-tenant-scoped target (User, say) has no
|
|
193
|
+
# default scope, which would turn approval into a cross-tenant write.
|
|
170
194
|
def target_class
|
|
171
|
-
target_type.
|
|
195
|
+
klass = target_type.to_s.safe_constantize
|
|
196
|
+
unless klass.is_a?(Class) && klass < OpenLoam::TenantRecord
|
|
197
|
+
raise OpenLoam::Error, "#{target_type.inspect} is not a tenant-scoped model — refusing to apply a staged change"
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
klass
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Staging is deliberately ungated ("propose" is not "apply"), so approval is
|
|
204
|
+
# where authority is spent and the APPROVER's field rules are what bind —
|
|
205
|
+
# otherwise staging routes around every one of them. Refusals raise rather
|
|
206
|
+
# than drop, so the proposal fails visibly instead of applying a lesser change.
|
|
207
|
+
def permitted_changeset(record)
|
|
208
|
+
blocked = changeset.keys & PROTECTED_FIELDS
|
|
209
|
+
raise OpenLoam::Error, "a staged change may not set #{blocked.join(', ')}" if blocked.any?
|
|
210
|
+
|
|
211
|
+
policy = OpenLoam::Policy.for(record)
|
|
212
|
+
refused = changeset.keys - policy.permitted_fields(changeset.keys).map(&:to_s)
|
|
213
|
+
raise OpenLoam::NotAuthorizedError, "the approver may not write #{refused.join(', ')}" if refused.any?
|
|
214
|
+
|
|
215
|
+
changeset
|
|
172
216
|
end
|
|
173
217
|
|
|
174
218
|
def load_target
|
|
@@ -178,31 +222,31 @@ module Loam
|
|
|
178
222
|
end
|
|
179
223
|
|
|
180
224
|
def encrypted_target_fields
|
|
181
|
-
return [] unless target_class.respond_to?(:
|
|
225
|
+
return [] unless target_class.respond_to?(:open_loam_encrypted_attributes)
|
|
182
226
|
|
|
183
|
-
target_class.
|
|
227
|
+
target_class.open_loam_encrypted_attributes.map(&:to_s)
|
|
184
228
|
end
|
|
185
229
|
|
|
186
230
|
# Set while a workflow transition is performing its save, so the validation
|
|
187
231
|
# below can tell a legitimate status move from a direct assignment.
|
|
188
|
-
def
|
|
189
|
-
@
|
|
232
|
+
def open_loam_perform_transition!(transition)
|
|
233
|
+
@open_loam_status_via_transition = true
|
|
190
234
|
super
|
|
191
235
|
ensure
|
|
192
|
-
@
|
|
236
|
+
@open_loam_status_via_transition = false
|
|
193
237
|
end
|
|
194
238
|
|
|
195
239
|
def status_changes_only_through_a_transition
|
|
196
240
|
return unless will_save_change_to_status?
|
|
197
|
-
return if @
|
|
241
|
+
return if @open_loam_status_via_transition
|
|
198
242
|
|
|
199
243
|
errors.add(:status, "may only change through an approval transition (approve!/reject!), not a direct write")
|
|
200
244
|
end
|
|
201
245
|
|
|
202
246
|
def status_starts_at_the_initial_state
|
|
203
|
-
return if status == self.class.
|
|
247
|
+
return if status == self.class.open_loam_workflow.initial
|
|
204
248
|
|
|
205
|
-
errors.add(:status, "must start at #{self.class.
|
|
249
|
+
errors.add(:status, "must start at #{self.class.open_loam_workflow.initial.inspect} — a staged action begins pending")
|
|
206
250
|
end
|
|
207
251
|
end
|
|
208
252
|
end
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
module
|
|
1
|
+
module OpenLoam
|
|
2
2
|
# A saved view of an entity's admin index — which columns show, active filters,
|
|
3
3
|
# sort, and page size — that a user can name, make their default, or share.
|
|
4
4
|
#
|
|
@@ -7,15 +7,15 @@ module Loam
|
|
|
7
7
|
# * "role" — everyone with the named membership role in the tenant.
|
|
8
8
|
# * "tenant" — everyone in the tenant.
|
|
9
9
|
# A private view is invisible to others by construction (see
|
|
10
|
-
#
|
|
10
|
+
# OpenLoam::Perspectives.visible_to), not merely by a permission check.
|
|
11
11
|
#
|
|
12
12
|
# Tenant-scoped like everything else, and audited. Optimistic-locked
|
|
13
13
|
# (lock_version) so two people editing a shared view don't silently clobber
|
|
14
14
|
# each other — the admin controller rescues ActiveRecord::StaleObjectError.
|
|
15
|
-
class Perspective <
|
|
16
|
-
self.table_name = "
|
|
15
|
+
class Perspective < OpenLoam::TenantRecord
|
|
16
|
+
self.table_name = "open_loam_perspectives"
|
|
17
17
|
|
|
18
|
-
include
|
|
18
|
+
include OpenLoam::Auditable
|
|
19
19
|
|
|
20
20
|
VISIBILITIES = %w[private role tenant].freeze
|
|
21
21
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
module
|
|
1
|
+
module OpenLoam
|
|
2
2
|
# Progress of a long-running job (a bulk import, a reindex, a report) so the
|
|
3
3
|
# admin can watch it live instead of guessing. Tenant-scoped; a job started in
|
|
4
4
|
# one tenant is only visible and streamable there.
|
|
@@ -7,11 +7,11 @@ module Loam
|
|
|
7
7
|
# ticks) and the audit trail would fill with noise. The terminal status and
|
|
8
8
|
# any error are captured on the row itself, which is the summary that matters.
|
|
9
9
|
#
|
|
10
|
-
# Each meaningful change publishes `
|
|
11
|
-
# carrying only id/percent/status — no record contents. See
|
|
10
|
+
# Each meaningful change publishes `open_loam.progress.updated` (broadcastable → SSE)
|
|
11
|
+
# carrying only id/percent/status — no record contents. See OpenLoam::Progress for
|
|
12
12
|
# the entry point.
|
|
13
|
-
class ProgressJob <
|
|
14
|
-
self.table_name = "
|
|
13
|
+
class ProgressJob < OpenLoam::TenantRecord
|
|
14
|
+
self.table_name = "open_loam_progress_jobs"
|
|
15
15
|
|
|
16
16
|
STATUSES = %w[running completed failed cancelled].freeze
|
|
17
17
|
STALE_AFTER = 5.minutes
|
|
@@ -93,11 +93,11 @@ module Loam
|
|
|
93
93
|
self
|
|
94
94
|
end
|
|
95
95
|
|
|
96
|
-
# Only ever id/percent/status leave the server (
|
|
96
|
+
# Only ever id/percent/status leave the server (OpenLoam::Events stamps tenant_id
|
|
97
97
|
# for the SSE deliverable filter; the frame's safe_payload drops everything
|
|
98
98
|
# else).
|
|
99
99
|
def broadcast
|
|
100
|
-
|
|
100
|
+
OpenLoam::Events.publish("open_loam.progress.updated", id: id, percent: percent, status: status)
|
|
101
101
|
end
|
|
102
102
|
end
|
|
103
103
|
end
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
module
|
|
1
|
+
module OpenLoam
|
|
2
2
|
# An advisory "someone is editing this" lock on a record — the COURTESY layer.
|
|
3
3
|
# The real guarantee against a silent clobber is optimistic locking
|
|
4
|
-
# (lock_version, see
|
|
4
|
+
# (lock_version, see OpenLoam::RecordLocks docs); this only warns the second
|
|
5
5
|
# editor. One lock per record, with a TTL: an expired lock is treated as free,
|
|
6
6
|
# and re-acquiring as the same holder extends it (a heartbeat). Plumbing, so
|
|
7
|
-
# not audited — like
|
|
8
|
-
class RecordLock <
|
|
9
|
-
self.table_name = "
|
|
7
|
+
# not audited — like OpenLoam::ApiToken.
|
|
8
|
+
class RecordLock < OpenLoam::TenantRecord
|
|
9
|
+
self.table_name = "open_loam_record_locks"
|
|
10
10
|
|
|
11
11
|
belongs_to :locked_by, class_name: "User"
|
|
12
12
|
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
module
|
|
1
|
+
module OpenLoam
|
|
2
2
|
# A recurring job that runs on a schedule (cron or interval). Tenant-scoped:
|
|
3
3
|
# a "tenant"-scope schedule enqueues its job in this tenant on schedule; a
|
|
4
4
|
# "system"-scope schedule enqueues once with no tenant context. Audited on
|
|
5
5
|
# config changes (not per run — runs are tracked by last_run_at/next_run_at).
|
|
6
6
|
#
|
|
7
7
|
# SECURITY: job_class is validated to be a known ActiveJob (see
|
|
8
|
-
#
|
|
8
|
+
# OpenLoam::Scheduler.resolve_job_class) at save AND at enqueue — a scheduler that
|
|
9
9
|
# constantized and ran arbitrary user input would be a code-execution hole.
|
|
10
|
-
class ScheduledJob <
|
|
11
|
-
self.table_name = "
|
|
10
|
+
class ScheduledJob < OpenLoam::TenantRecord
|
|
11
|
+
self.table_name = "open_loam_scheduled_jobs"
|
|
12
12
|
|
|
13
|
-
include
|
|
13
|
+
include OpenLoam::Auditable
|
|
14
14
|
|
|
15
15
|
SCOPES = %w[tenant system].freeze
|
|
16
16
|
LOCK_TTL = 5.minutes # how long a claim holds a due job before another tick may retry it
|
|
@@ -26,11 +26,11 @@ module Loam
|
|
|
26
26
|
# The next fire time strictly after `from`, in this job's timezone (UTC when
|
|
27
27
|
# unset). Stored in next_run_at as UTC.
|
|
28
28
|
def compute_next_run(from = Time.current)
|
|
29
|
-
|
|
29
|
+
OpenLoam::Cron.next_after(schedule, from: from, zone: timezone.presence || "UTC")
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def job_class_constant
|
|
33
|
-
|
|
33
|
+
OpenLoam::Scheduler.resolve_job_class(job_class)
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
private
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
module OpenLoam
|
|
2
|
+
# The word-level search index maintained by OpenLoam::Search::TokenDriver: one row
|
|
3
|
+
# per (record, token), tenant-scoped like every OpenLoam entity. Plumbing — not
|
|
4
|
+
# audited, not soft-deletable. Only the TokenDriver reads or writes it; under
|
|
5
|
+
# the default LikeDriver this table simply stays empty.
|
|
6
|
+
class SearchToken < OpenLoam::TenantRecord
|
|
7
|
+
self.table_name = "open_loam_search_tokens"
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
module
|
|
1
|
+
module OpenLoam
|
|
2
2
|
# Links an external IdP identity (a provider's stable `sub`) to a local User.
|
|
3
3
|
# The durable key SSO callbacks resolve on — an email can change at the IdP,
|
|
4
4
|
# `sub` does not. Tenant-scoped (created and read inside the provider's tenant).
|
|
5
|
-
class SsoIdentity <
|
|
6
|
-
self.table_name = "
|
|
5
|
+
class SsoIdentity < OpenLoam::TenantRecord
|
|
6
|
+
self.table_name = "open_loam_sso_identities"
|
|
7
7
|
|
|
8
8
|
belongs_to :user
|
|
9
|
-
belongs_to :sso_provider, class_name: "
|
|
9
|
+
belongs_to :sso_provider, class_name: "OpenLoam::SsoProvider"
|
|
10
10
|
|
|
11
11
|
validates :sub, presence: true, uniqueness: { scope: :sso_provider_id }
|
|
12
12
|
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
module OpenLoam
|
|
2
|
+
# A tenant's connection to an external identity provider (OIDC). Per-tenant
|
|
3
|
+
# config: each tenant/org wires its OWN IdP, so the client_secret is encrypted
|
|
4
|
+
# under the tenant key (the default Encryptable scope) — unlike MFA, which is
|
|
5
|
+
# user-keyed. Home-realm discovery matches an email `domain` to the owning
|
|
6
|
+
# provider at the sign-in page (see OpenLoam::Sso.provider_for).
|
|
7
|
+
class SsoProvider < OpenLoam::TenantRecord
|
|
8
|
+
self.table_name = "open_loam_sso_providers"
|
|
9
|
+
|
|
10
|
+
include OpenLoam::Auditable # config changes are audited; the secret is redacted
|
|
11
|
+
include OpenLoam::Encryptable
|
|
12
|
+
|
|
13
|
+
# Tenant-scoped key (default): SSO config belongs to the tenant, and the
|
|
14
|
+
# secret is only ever read inside that tenant's context on callback.
|
|
15
|
+
encrypts :client_secret
|
|
16
|
+
|
|
17
|
+
has_many :identities, class_name: "OpenLoam::SsoIdentity",
|
|
18
|
+
foreign_key: :sso_provider_id, dependent: :delete_all
|
|
19
|
+
|
|
20
|
+
validates :name, :protocol, :domain, :jit_role, presence: true
|
|
21
|
+
# Globally unique so HRD resolves exactly one owning IdP per domain. Rails'
|
|
22
|
+
# uniqueness validator queries WITHOUT the default tenant scope, so this
|
|
23
|
+
# catches a collision across tenants too.
|
|
24
|
+
validates :domain, uniqueness: true
|
|
25
|
+
|
|
26
|
+
normalizes :domain, with: ->(domain) { domain.to_s.strip.downcase.presence }
|
|
27
|
+
|
|
28
|
+
# The server fetches the issuer's discovery document and posts the code
|
|
29
|
+
# exchange to it, so a manager-typed issuer is an SSRF vector unless it is
|
|
30
|
+
# provably external. https because the client_secret travels over it.
|
|
31
|
+
validate :issuer_is_reachable_from_outside, if: -> { issuer.present? }
|
|
32
|
+
|
|
33
|
+
# Editing the domain drops the proof — otherwise a verified provider could be
|
|
34
|
+
# repointed at a domain nobody approved.
|
|
35
|
+
before_validation :reset_domain_verification, if: -> { persisted? && domain_changed? }
|
|
36
|
+
|
|
37
|
+
scope :active, -> { where(active: true) }
|
|
38
|
+
scope :domain_verified, -> { where.not(domain_verified_at: nil) }
|
|
39
|
+
|
|
40
|
+
# Until ownership is proven, HRD skips the provider and OpenLoam::Sso refuses
|
|
41
|
+
# to link an existing account through it.
|
|
42
|
+
def domain_verified? = domain_verified_at.present?
|
|
43
|
+
|
|
44
|
+
# Operator entry point (rake open_loam:sso:verify_domain), off the admin path.
|
|
45
|
+
def verify_domain!(at: Time.current)
|
|
46
|
+
update!(domain_verified_at: at)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# IdP group -> OpenLoam role, first match wins; falls back to jit_role.
|
|
50
|
+
def group_roles
|
|
51
|
+
group_role_map.is_a?(Hash) ? group_role_map : {}
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def reset_domain_verification
|
|
57
|
+
self.domain_verified_at = nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def issuer_is_reachable_from_outside
|
|
61
|
+
OpenLoam::OutboundUrl.validate!(issuer, require_https: true)
|
|
62
|
+
rescue OpenLoam::OutboundUrl::BlockedError => error
|
|
63
|
+
errors.add(:issuer, error.message)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module OpenLoam
|
|
2
|
+
# The tenant itself is deliberately NOT tenant-scoped — it is the axis the
|
|
3
|
+
# rest of the system is scoped by.
|
|
4
|
+
class Tenant < ActiveRecord::Base
|
|
5
|
+
include OpenLoam::GeneratedKey
|
|
6
|
+
self.table_name = "open_loam_tenants"
|
|
7
|
+
|
|
8
|
+
validates :name, presence: true
|
|
9
|
+
validates :slug, presence: true, uniqueness: true
|
|
10
|
+
|
|
11
|
+
has_many :memberships, class_name: "OpenLoam::Membership", dependent: :delete_all
|
|
12
|
+
|
|
13
|
+
# A new tenant is never empty: whatever the app registered via
|
|
14
|
+
# OpenLoam.on_tenant_created runs here, inside this tenant's context. After
|
|
15
|
+
# commit, so callbacks see a persisted tenant and may enqueue jobs.
|
|
16
|
+
# `bin/rails open_loam:sync` re-runs the same callbacks for existing tenants —
|
|
17
|
+
# which is why they must be idempotent (see OpenLoam::Lifecycle).
|
|
18
|
+
after_create_commit { OpenLoam::Lifecycle.run_tenant_created(self) }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
module
|
|
1
|
+
module OpenLoam
|
|
2
2
|
# A per-locale override for ONE field of ONE record — the storage behind
|
|
3
|
-
#
|
|
3
|
+
# OpenLoam::Translatable. The record's own column holds the base (default-locale)
|
|
4
4
|
# value; a translation row overrides it for a specific locale. Additive:
|
|
5
5
|
# writing a translation never touches the base column, so the base value is
|
|
6
6
|
# never lost. Tenant-scoped and audited.
|
|
7
|
-
class Translation <
|
|
8
|
-
self.table_name = "
|
|
7
|
+
class Translation < OpenLoam::TenantRecord
|
|
8
|
+
self.table_name = "open_loam_translations"
|
|
9
9
|
|
|
10
|
-
include
|
|
10
|
+
include OpenLoam::Auditable
|
|
11
11
|
|
|
12
12
|
belongs_to :translatable, polymorphic: true
|
|
13
13
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module OpenLoam
|
|
2
|
+
# Where one tenant wants its domain events delivered. `event_pattern` uses
|
|
3
|
+
# the same rule as OpenLoam::Events.subscribe — a trailing dot is a domain prefix
|
|
4
|
+
# ("rental."), anything else is an exact event name.
|
|
5
|
+
#
|
|
6
|
+
# The secret signs every delivery (X-OpenLoam-Signature), so a receiver can tell
|
|
7
|
+
# a real call from a forged one.
|
|
8
|
+
class WebhookEndpoint < OpenLoam::TenantRecord
|
|
9
|
+
self.table_name = "open_loam_webhook_endpoints"
|
|
10
|
+
|
|
11
|
+
validates :url, presence: true
|
|
12
|
+
validates :event_pattern, presence: true
|
|
13
|
+
|
|
14
|
+
# The server fetches this URL, so a tenant must not be able to aim it at an
|
|
15
|
+
# address only the server can reach. Rejected at save for a clear error;
|
|
16
|
+
# OpenLoam::WebhookDeliveryJob re-checks at delivery, because DNS moves.
|
|
17
|
+
validate :url_is_reachable_from_outside, if: -> { url.present? }
|
|
18
|
+
|
|
19
|
+
scope :active, -> { where(active: true) }
|
|
20
|
+
|
|
21
|
+
before_validation on: :create do
|
|
22
|
+
self.secret ||= SecureRandom.hex(32)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# One matcher, shared with the event bus, so "rental." means the same thing
|
|
26
|
+
# to a subscriber and to a webhook.
|
|
27
|
+
def matches?(event_name)
|
|
28
|
+
OpenLoam::Events.pattern_matches?(event_pattern, event_name)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def url_is_reachable_from_outside
|
|
34
|
+
OpenLoam::OutboundUrl.validate!(url)
|
|
35
|
+
rescue OpenLoam::OutboundUrl::BlockedError => error
|
|
36
|
+
errors.add(:url, error.message)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Uploading happens through the entity's own form (it is an update, and the
|
|
3
3
|
entity's policy decides who may do it); this partial only lists what is
|
|
4
4
|
already attached. Note that these links are signed URLs, not tenant-checked
|
|
5
|
-
addresses — see
|
|
5
|
+
addresses — see OpenLoam::Attachable. %>
|
|
6
6
|
<h2>Files</h2>
|
|
7
7
|
|
|
8
8
|
<% if record.files.attached? %>
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<p style="color:#b00"><%= flash[:alert] %></p>
|
|
9
9
|
<% end %>
|
|
10
10
|
|
|
11
|
-
<% record.
|
|
11
|
+
<% record.open_loam_comments.oldest_first.each do |comment| %>
|
|
12
12
|
<p>
|
|
13
13
|
<strong><%= User.find_by(id: comment.author_id)&.name || "—" %></strong>
|
|
14
14
|
<em><%= comment.created_at.to_fs(:short) %></em><br>
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
rendering lives once in the gem instead of being inlined per entity. %>
|
|
4
4
|
<% mode = local_assigns.fetch(:mode, :edit) %>
|
|
5
5
|
<% record.class.custom_field_definitions.each do |definition| %>
|
|
6
|
+
<%# Not readable by this role: omit the field entirely. Falling through to the
|
|
7
|
+
read-only branch below would print the value to anyone who merely lacks
|
|
8
|
+
WRITE access, which is a different question. %>
|
|
9
|
+
<% next unless policy.custom_field_readable?(definition.name) %>
|
|
6
10
|
<% param_name = "#{record.model_name.param_key}[custom_fields][#{definition.name}]" %>
|
|
7
11
|
<% value = record.custom_field(definition.name) %>
|
|
8
12
|
<p>
|
|
@@ -20,14 +24,14 @@
|
|
|
20
24
|
<% when "integer", "decimal" %>
|
|
21
25
|
<%= number_field_tag param_name, value %>
|
|
22
26
|
<% when "dictionary" %>
|
|
23
|
-
<% options =
|
|
27
|
+
<% options = OpenLoam::Dictionaries.entries(definition.dictionary_key).map { |e| [e.label, e.value] } %>
|
|
24
28
|
<%= select_tag param_name, options_for_select(options, value), include_blank: true, id: param_name %>
|
|
25
29
|
<% else %>
|
|
26
30
|
<%= text_field_tag param_name, value %>
|
|
27
31
|
<% end %>
|
|
28
32
|
<% else %>
|
|
29
33
|
<%# A dictionary field shows its entry label, not the raw stored code. %>
|
|
30
|
-
<% display = definition.field_type == "dictionary" ?
|
|
34
|
+
<% display = definition.field_type == "dictionary" ? OpenLoam::Dictionaries.label_for(definition.dictionary_key, value) : value %>
|
|
31
35
|
<%= definition.name.humanize %>: <%= display %>
|
|
32
36
|
<% if mode == :edit %><em>(read-only for your role)</em><% end %>
|
|
33
37
|
<% end %>
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
require "rails/generators"
|
|
2
2
|
require "rails/generators/active_record"
|
|
3
|
-
require "generators/
|
|
3
|
+
require "generators/open_loam/primary_key_options"
|
|
4
4
|
|
|
5
|
-
module
|
|
5
|
+
module OpenLoam
|
|
6
6
|
module Generators
|
|
7
|
-
# `rails g
|
|
7
|
+
# `rails g open_loam:entity Equipment name:string daily_rate:decimal --domain rental`
|
|
8
8
|
#
|
|
9
9
|
# THE interface for adding a business entity — for humans and AI agents
|
|
10
10
|
# alike. One command produces a tenant-scoped, audited, evented model, its
|
|
11
11
|
# policy, an admin screen, and the isolation tests that prove the
|
|
12
|
-
# guardrails hold. No decisions about how tenancy/permissions work:
|
|
12
|
+
# guardrails hold. No decisions about how tenancy/permissions work: OpenLoam
|
|
13
13
|
# already decided.
|
|
14
14
|
class EntityGenerator < Rails::Generators::NamedBase
|
|
15
15
|
include ActiveRecord::Generators::Migration
|
|
16
|
-
include
|
|
16
|
+
include OpenLoam::Generators::PrimaryKeyOptions
|
|
17
17
|
|
|
18
18
|
source_root File.expand_path("templates", __dir__)
|
|
19
19
|
|
|
@@ -21,7 +21,7 @@ module Loam
|
|
|
21
21
|
class_option :domain, type: :string, default: "app",
|
|
22
22
|
desc: "Event domain prefix (-> domain.entity.created)"
|
|
23
23
|
class_option :encrypt, type: :array, default: [], banner: "field field",
|
|
24
|
-
desc: "Encrypt these fields at rest, per tenant (
|
|
24
|
+
desc: "Encrypt these fields at rest, per tenant (OpenLoam::Encryptable)"
|
|
25
25
|
class_option :encrypt_searchable, type: :array, default: [], banner: "field field",
|
|
26
26
|
desc: "Encrypt + add a blind index for exact-match lookup"
|
|
27
27
|
|
|
@@ -59,7 +59,7 @@ module Loam
|
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
# `namespace:` makes Rails inject the route INTO the existing
|
|
62
|
-
# `namespace :admin do` block (the one
|
|
62
|
+
# `namespace :admin do` block (the one open_loam:install wrote) instead of
|
|
63
63
|
# stacking one admin block per entity, and falls back to creating the
|
|
64
64
|
# block when there is none. Re-running the generator is a no-op: the
|
|
65
65
|
# injection is `force: false`, so identical routing code is skipped.
|
|
@@ -70,7 +70,7 @@ module Loam
|
|
|
70
70
|
# identical `resources :gadgets` lines would leave the second one out.
|
|
71
71
|
#
|
|
72
72
|
# The admin resource carries the soft-delete recycle bin: `deleted` lists
|
|
73
|
-
# `only_deleted`, `restore` brings one back (see
|
|
73
|
+
# `only_deleted`, `restore` brings one back (see OpenLoam::SoftDeletable).
|
|
74
74
|
def add_route
|
|
75
75
|
route <<~RUBY.strip, namespace: :admin
|
|
76
76
|
resources :#{plural_file_name} do
|
|
@@ -101,7 +101,7 @@ module Loam
|
|
|
101
101
|
attributes.map(&:name)
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
-
# Only text-ish columns are worth a LIKE (see
|
|
104
|
+
# Only text-ish columns are worth a LIKE (see OpenLoam::Searchable), so an
|
|
105
105
|
# entity with none gets no `searchable_by` declaration and stays out of
|
|
106
106
|
# the global search. Encrypted columns are excluded: ciphertext is
|
|
107
107
|
# meaningless to LIKE, and declaring both would raise at class load.
|
|
@@ -8,8 +8,9 @@ module Api
|
|
|
8
8
|
before_action :set_record, only: %i[show update destroy]
|
|
9
9
|
|
|
10
10
|
def index
|
|
11
|
+
authorize!(policy_for(<%= class_name %>.new), :read?)
|
|
11
12
|
records = <%= class_name %>.order(created_at: :desc).to_a
|
|
12
|
-
enriched =
|
|
13
|
+
enriched = OpenLoam::Enrichers.enrich_many(records) # one batched pass, no N+1
|
|
13
14
|
render json: records.map { |record| entity_json(record, enrichments: enriched[record.id]) }
|
|
14
15
|
end
|
|
15
16
|
|