This commit is contained in:
@@ -3,6 +3,18 @@ export interface ConfiguredTransportUrlOptions {
|
||||
originOnly?: boolean;
|
||||
}
|
||||
|
||||
export function parseCredentialFreeHttpUrl(value: string): URL | undefined {
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(value);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
if (!["http:", "https:"].includes(url.protocol)
|
||||
|| url.username || url.password || url.search || url.hash) return undefined;
|
||||
return url;
|
||||
}
|
||||
|
||||
function canonicalLoopbackAuthority(value: string): boolean {
|
||||
const match = /^http:\/\/([^/?#]+)(?:[/?#]|$)/.exec(value);
|
||||
if (!match) return false;
|
||||
@@ -27,15 +39,8 @@ export function parseConfiguredTransportUrl(
|
||||
value: string,
|
||||
options: ConfiguredTransportUrlOptions,
|
||||
): URL | undefined {
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(value);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
if (url.username || url.password || url.search || url.hash || (options.originOnly && url.pathname !== "/")) {
|
||||
return undefined;
|
||||
}
|
||||
const url = parseCredentialFreeHttpUrl(value);
|
||||
if (!url || (options.originOnly && url.pathname !== "/")) return undefined;
|
||||
if (url.protocol === "https:") return url;
|
||||
if (options.allowLoopbackHttp && url.protocol === "http:" && canonicalLoopbackAuthority(value)) return url;
|
||||
return undefined;
|
||||
|
||||
@@ -159,7 +159,7 @@ export class CatalogService {
|
||||
for (const id of Object.values(CATALOG_SECRET_IDS)) this.secretStore.forget(workspaceId, id);
|
||||
}
|
||||
|
||||
async test(database: WorkspaceDatabase): Promise<DatabaseTestResult> {
|
||||
async test(database: WorkspaceDatabase): Promise<WorkspaceDatabase | undefined> {
|
||||
return await this.operations.run(database.id, async () => {
|
||||
const testedAt = new Date().toISOString();
|
||||
const controller = new AbortController();
|
||||
@@ -168,6 +168,7 @@ export class CatalogService {
|
||||
? database.binding.restAuth === "none" ? [] : [CATALOG_SECRET_IDS.apiKey]
|
||||
: [];
|
||||
const materialized = this.secretStore.materialize(database.workspaceId, required);
|
||||
let result: DatabaseTestResult;
|
||||
try {
|
||||
if (database.binding.transport !== "rest_api") {
|
||||
const client = await this.postgres.connect(database, controller.signal);
|
||||
@@ -205,13 +206,13 @@ export class CatalogService {
|
||||
},
|
||||
});
|
||||
}
|
||||
return {
|
||||
result = {
|
||||
connectionStatus: "reachable",
|
||||
testedVersion: database.version,
|
||||
lastTestedAt: testedAt,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
result = {
|
||||
connectionStatus: "failed",
|
||||
testedVersion: database.version,
|
||||
lastTestedAt: testedAt,
|
||||
@@ -223,6 +224,7 @@ export class CatalogService {
|
||||
controller.abort();
|
||||
materialized.release();
|
||||
}
|
||||
return await this.repository.recordTest(database.id, database.version, result);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { isPrincipalContext, requirePermission } from "../auth/authorization.js";
|
||||
import { parseCredentialFreeHttpUrl } from "../auth/url-policy.js";
|
||||
import { CatalogService, type CatalogSecretName } from "../catalog/service.js";
|
||||
import { WorkspaceRegistryError } from "../workspaces/git-repository.js";
|
||||
import {
|
||||
@@ -26,7 +27,9 @@ const bindingSchema = z.object({
|
||||
host: optionalText,
|
||||
port: port.optional(),
|
||||
username: optionalText,
|
||||
baseUrl: z.url().max(2048).optional(),
|
||||
baseUrl: z.string().max(2048)
|
||||
.refine((value) => parseCredentialFreeHttpUrl(value) !== undefined)
|
||||
.optional(),
|
||||
restPath: z.string().regex(/^\/(?!\/)[^?#\\\u0000-\u001f]*$/).max(512).optional(),
|
||||
restAuth: z.enum(["none", "bearer", "x-api-key"]).optional(),
|
||||
tlsServername: optionalText,
|
||||
@@ -209,7 +212,7 @@ export function catalogDatabaseRoutes(
|
||||
const database = await deps.repository.get(id);
|
||||
if (!database) return reply.code(404).send({ code: "database_not_found", message: "Database configuration was not found." });
|
||||
if (database.version !== version) return reply.code(409).send({ code: "database_stale", message: "Database configuration changed. Reload and try again." });
|
||||
const tested = await deps.repository.recordTest(id, version, await deps.service.test(database));
|
||||
const tested = await deps.service.test(database);
|
||||
if (!tested) return reply.code(409).send({ code: "database_stale", message: "Database configuration changed. Reload and try again." });
|
||||
return { ...tested, configured: true, secrets: deps.service.configuredSecrets(tested.workspaceId) };
|
||||
} catch (error) { return safeError(reply, error); }
|
||||
|
||||
Reference in New Issue
Block a user