mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
feat(seed): seed mock SAML connections on the dummy project
Adds back seedSamlConnections + STACK_SEED_ENABLE_SAML callsite. This PR is the first one in the stack where auth.saml.connections.* exists in the config schema, so the writes actually take effect (on the mock IdP PR they were silently dropped during normalization). Default mock URL is now derived from NEXT_PUBLIC_STACK_PORT_PREFIX + the mock's new port suffix 42, instead of being hardcoded to http://localhost:8115. STACK_MOCK_SAML_URL still wins as an explicit override. Metadata fetches now use a 10s AbortSignal.timeout and encodeURIComponent the slug into the URL.
This commit is contained in:
parent
7bcc84fb28
commit
841d0591d0
@ -1939,6 +1939,74 @@ async function seedBulkSignupsAndActivity(options: {
|
||||
console.log(`[seed-activity] Events: $token-refresh=${tokenRefreshCount} $page-view=${pageViewCount} $click=${clickCount} total=${clickhouseRows.length}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-creates two SAML connections (acme + globex) on the dummy project that
|
||||
* point at the local mock SAML IdP. Gated on STACK_SEED_ENABLE_SAML='true'.
|
||||
* Fetches the mock IdP's metadata at seed time so the seeded cert matches
|
||||
* the cert the mock generated at startup — the mock currently regenerates
|
||||
* keys per restart, so re-seed if you restart the mock.
|
||||
*/
|
||||
async function seedSamlConnections(projectId: string): Promise<void> {
|
||||
// Default URL is derived from NEXT_PUBLIC_STACK_PORT_PREFIX so a non-default
|
||||
// prefix (e.g. running multiple dev environments side by side) doesn't make
|
||||
// the seed reach into a stale service from a different stack.
|
||||
const portPrefix = getEnvVariable("NEXT_PUBLIC_STACK_PORT_PREFIX", "81");
|
||||
const mockUrl = getEnvVariable("STACK_MOCK_SAML_URL", `http://localhost:${portPrefix}42`);
|
||||
const tenants: Array<{ slug: string, displayName: string, domain: string }> = [
|
||||
{ slug: "acme", displayName: "Acme Corp SSO", domain: "acme.test" },
|
||||
{ slug: "globex", displayName: "Globex SAML", domain: "globex.test" },
|
||||
];
|
||||
|
||||
const fetched = await Promise.all(
|
||||
tenants.map(async (t) => {
|
||||
const metadataUrl = new URL(`/idp/${encodeURIComponent(t.slug)}/metadata`, mockUrl);
|
||||
const res = await fetch(metadataUrl, { signal: AbortSignal.timeout(10_000) });
|
||||
if (!res.ok) {
|
||||
throw new Error(`Mock SAML IdP at ${metadataUrl.toString()} returned ${res.status} — is the mock running?`);
|
||||
}
|
||||
const xml = await res.text();
|
||||
// Inline minimal metadata parse to avoid a circular import. Format is
|
||||
// exactly what the mock emits, so a regex is enough; the production
|
||||
// parser at apps/backend/src/saml/metadata-parser.tsx is the
|
||||
// robust one used by the dashboard "paste metadata" form.
|
||||
const entityIdMatch = xml.match(/entityID="([^"]+)"/);
|
||||
const ssoUrlMatch = xml.match(/Binding="urn:oasis:names:tc:SAML:2\.0:bindings:HTTP-Redirect"[^>]*Location="([^"]+)"/);
|
||||
const certMatch = xml.match(/<X509Certificate>([\s\S]+?)<\/X509Certificate>/);
|
||||
if (!entityIdMatch || !ssoUrlMatch || !certMatch) {
|
||||
throw new Error(`Could not parse mock IdP metadata for tenant ${t.slug}`);
|
||||
}
|
||||
return {
|
||||
...t,
|
||||
idpEntityId: entityIdMatch[1],
|
||||
idpSsoUrl: ssoUrlMatch[1],
|
||||
idpCertificate: certMatch[1].replace(/\s+/g, ""),
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
// Set the entire connection entry as a single value, not as deep
|
||||
// dot-keys — config normalization with onDotIntoNonObject="ignore"
|
||||
// drops dot-keys that try to navigate into a record entry that
|
||||
// doesn't yet exist (same convention as auth.oauth.providers).
|
||||
const overlay: Parameters<typeof overrideEnvironmentConfigOverride>[0]["environmentConfigOverrideOverride"] = {};
|
||||
for (const f of fetched) {
|
||||
overlay[`auth.saml.connections.${f.slug}`] = {
|
||||
displayName: f.displayName,
|
||||
allowSignIn: true,
|
||||
domain: f.domain,
|
||||
idpEntityId: f.idpEntityId,
|
||||
idpSsoUrl: f.idpSsoUrl,
|
||||
idpCertificate: f.idpCertificate,
|
||||
};
|
||||
}
|
||||
|
||||
await overrideEnvironmentConfigOverride({
|
||||
projectId,
|
||||
branchId: DEFAULT_BRANCH_ID,
|
||||
environmentConfigOverrideOverride: overlay,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new project and fills it with dummy data (users, teams, payments, emails, analytics events).
|
||||
* Used by both the seed script and the preview project creation endpoint.
|
||||
@ -2090,6 +2158,16 @@ export async function seedDummyProject(options: SeedDummyProjectOptions): Promis
|
||||
}),
|
||||
]);
|
||||
|
||||
// Run sequentially after the parallel block. Both this and the
|
||||
// `payments.testMode` write above target the same environment config,
|
||||
// and `overrideEnvironmentConfigOverride` is read-modify-write — running
|
||||
// them in parallel races and one write clobbers the other (TODO at
|
||||
// config.ts:491 already documents this). Sequencing avoids the race
|
||||
// until the underlying override is wrapped in a serializable txn.
|
||||
if (getEnvVariable("STACK_SEED_ENABLE_SAML", "false") === "true") {
|
||||
await seedSamlConnections(projectId);
|
||||
}
|
||||
|
||||
await seedDummyTransactions({
|
||||
prisma: dummyPrisma,
|
||||
tenancyId: dummyTenancy.id,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user