// Clean per-tenant installer for saltcorn-idp on the Postgres multi-tenant // instance. Registers + enables the plugin in each named tenant schema and runs // its onLoad (creating the _idp_* tables + bootstrapping keys), using Saltcorn's // supported Plugin.loadAndSaveNewPlugin API inside runWithTenant -- replacing the // old manual "INSERT INTO ._sc_plugins" SQL hack. // // The CLI `install-plugin -t -d ` cannot do this: a local (-d) // plugin is "unsafe" on a non-root tenant, so loadAndSaveNewPlugin returns // before the upsert unless allowUnsafeOnTenantsWithoutConfigSetting (its 5th arg) // is set, and the CLI never passes it. This script passes it. // // Usage (from project root, PG env sourced -- see installIdpTenant.sh): // node idp/scripts/installIdpTenant.js t1 t2 (or '*' for all tenants) const { createRequire } = require("node:module"); const path = require("node:path"); // Re-root @saltcorn/* against the Saltcorn checkout's node_modules. This file // lives at idp/scripts/, so up 2 = project root, then saltcorn/packages/... const scRequire = createRequire(path.join(__dirname, "..", "..", "saltcorn", "packages", "saltcorn-data", "package.json")); const Plugin = scRequire("@saltcorn/data/models/plugin"); const db = scRequire("@saltcorn/data/db"); const { init_multi_tenant, getRootState } = scRequire("@saltcorn/data/db/state"); const { getAllTenants } = scRequire("@saltcorn/admin-models/models/tenant"); const PLUGIN_NAME = "saltcorn-idp"; const IDP_DIR = path.resolve(__dirname, ".."); const installInto = async (tenant) => { await db.runWithTenant(tenant, async () => { await db.withTransaction(async () => { // Remove any prior rows (including the old manual-hack row and earlier // installs) so we converge on exactly one _sc_plugins row -- no // duplicate source of truth. await db.deleteWhere("_sc_plugins", { name: PLUGIN_NAME }); const plugin = new Plugin({ name: PLUGIN_NAME, source: "local", location: IDP_DIR, configuration: {} }); await Plugin.loadAndSaveNewPlugin(plugin, true, false); }); // Verify against the NEWEST table so a stale Phase-3 row can't pass. const row = await db.selectMaybeOne("_sc_plugins", { name: PLUGIN_NAME }); const svc = await db.query( "SELECT 1 FROM information_schema.tables WHERE table_schema = $1 AND table_name = '_idp_ldap_service'", [db.getTenantSchema()] ); const hasSvc = svc && svc.rows && svc.rows.length > 0; // eslint-disable-next-line no-console console.log(`[installIdpTenant] ${tenant}: _sc_plugins=${row ? "yes" : "NO"} _idp_ldap_service=${hasSvc ? "yes" : "NO"}`); if (!row || !hasSvc) { throw new Error("install verification failed for tenant " + tenant + " (onLoad did not run)"); } }); }; const main = async () => { await Plugin.loadAllPlugins(); // Resolve the target tenants (from the public _sc_tenants list). let tenants = process.argv.slice(2); if (tenants.length === 0 || (tenants.length === 1 && tenants[0] === "*")) { const all = await db.runWithTenant(db.connectObj.default_schema, getAllTenants); tenants = (all || []).map((t) => (typeof t === "string" ? t : t.subdomain)).filter(Boolean); } if (!tenants || tenants.length === 0) { // eslint-disable-next-line no-console console.error("[installIdpTenant] no tenants to install into"); process.exit(1); } // Initialize per-tenant State (so getState() resolves inside runWithTenant) // without running migrations. This also runs each tenant's existing plugins' // onLoad, which is itself idempotent. await init_multi_tenant(Plugin.loadAllPlugins, true, tenants); // Permit installing this LOCAL plugin into tenant schemas. In this Saltcorn // build loadAndSaveNewPlugin skips any non-"npm" plugin on a non-root tenant // BEFORE the allowUnsafe arg is consulted; the supported lever is this // root-only config -- the intended setting for a multi-tenant deployment that // offers the IdP plugin to its tenants. await getRootState().setConfig("tenants_unsafe_plugins", true); // eslint-disable-next-line no-console console.log("[installIdpTenant] installing " + PLUGIN_NAME + " into: " + tenants.join(", ")); for (const t of tenants) { await installInto(t); } // eslint-disable-next-line no-console console.log("[installIdpTenant] done"); process.exit(0); }; main().catch((e) => { // eslint-disable-next-line no-console console.error("[installIdpTenant] ERROR:", e && (e.stack || e.message || e)); process.exit(1); });