Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 159 additions & 11 deletions infra/relay/src/environments/EnvironmentConnector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ const decodeMintRequestBody = Schema.decodeUnknownSync(
const isEnvironmentConnectNotAuthorized = Schema.is(
EnvironmentConnector.EnvironmentConnectNotAuthorized,
);
const isEnvironmentMintRequestFailed = Schema.is(EnvironmentConnector.EnvironmentMintRequestFailed);
const isEnvironmentMintResponseInvalid = Schema.is(
EnvironmentConnector.EnvironmentMintResponseInvalid,
);

function requestBodyText(request: HttpClientRequest.HttpClientRequest): string {
return request.body._tag === "Uint8Array" ? new TextDecoder().decode(request.body.body) : "{}";
Expand Down Expand Up @@ -227,6 +231,55 @@ function makeLinks(
}

describe("EnvironmentConnector", () => {
it("redacts endpoint secrets while preserving mapped causes", () => {
const httpBaseUrl =
"https://environment-user:environment-password@env.example.test/private/workspace?access_token=environment-secret#environment-fragment";
const cause = new Error("mint failed");
const requestError = EnvironmentConnector.EnvironmentMintRequestFailed.fromEndpoint({
userId: "user_123",
environmentId: "env-connector-test",
operation: "connect",
stage: "send_request",
httpBaseUrl,
cause,
});
const responseError = EnvironmentConnector.EnvironmentMintResponseInvalid.fromEndpoint({
userId: "user_123",
environmentId: "env-connector-test",
operation: "connect",
httpBaseUrl,
reason: "proof_verification_failed",
cause,
});
const timeoutError = EnvironmentConnector.EnvironmentMintRequestTimedOut.fromEndpoint({
userId: "user_123",
environmentId: "env-connector-test",
httpBaseUrl,
timeoutMs: EnvironmentConnector.ENVIRONMENT_MINT_REQUEST_TIMEOUT_MS,
});

for (const error of [requestError, responseError, timeoutError]) {
expect(error).toMatchObject({
httpBaseUrlInputLength: httpBaseUrl.length,
httpBaseUrlProtocol: "https:",
httpBaseUrlHostname: "env.example.test",
});
const serialized = JSON.stringify(error);
for (const secret of [
"environment-user",
"environment-password",
"/private/workspace",
"environment-secret",
"environment-fragment",
]) {
expect(serialized).not.toContain(secret);
expect(error.message).not.toContain(secret);
}
}
expect(requestError.cause).toBe(cause);
expect(responseError.cause).toBe(cause);
});

it.effect("loads the environment link and managed allocation concurrently", () =>
Effect.gen(function* () {
const started = yield* Ref.make(0);
Expand Down Expand Up @@ -401,24 +454,57 @@ describe("EnvironmentConnector", () => {
const resolutionSpan = spans.find(
(span) => span.name === "relay.environment_connector.resolve_managed_endpoint",
);
expect(Object.fromEntries(resolutionSpan?.attributes ?? [])).toMatchObject({
const resolutionAttributes = Object.fromEntries(resolutionSpan?.attributes ?? []);
expect(resolutionAttributes).toMatchObject({
"relay.authorization.allocation_hostname": "env.example.test",
"relay.authorization.allocation_has_ready_at": true,
"relay.authorization.allocation_has_tunnel_id": true,
"relay.authorization.allocation_has_dns_record_id": true,
"relay.authorization.linked_http_base_url": "https://attacker.example.test/",
"relay.authorization.linked_ws_base_url": "wss://attacker.example.test/ws",
"relay.authorization.resolved_http_base_url": "https://env.example.test/",
"relay.authorization.resolved_ws_base_url": "wss://env.example.test/ws",
"relay.authorization.linked_http_base_url.input_length":
"https://attacker-user:attacker-password@attacker.example.test/private/workspace?access_token=attacker-secret#attacker-fragment"
.length,
"relay.authorization.linked_http_base_url.protocol": "https:",
"relay.authorization.linked_http_base_url.hostname": "attacker.example.test",
"relay.authorization.linked_ws_base_url.input_length":
"wss://socket-user:socket-password@attacker.example.test/private/socket?access_token=socket-secret#socket-fragment"
.length,
"relay.authorization.linked_ws_base_url.protocol": "wss:",
"relay.authorization.linked_ws_base_url.hostname": "attacker.example.test",
"relay.authorization.resolved_http_base_url.input_length": "https://env.example.test/"
.length,
"relay.authorization.resolved_http_base_url.protocol": "https:",
"relay.authorization.resolved_http_base_url.hostname": "env.example.test",
"relay.authorization.resolved_ws_base_url.input_length": "wss://env.example.test/ws".length,
"relay.authorization.resolved_ws_base_url.protocol": "wss:",
"relay.authorization.resolved_ws_base_url.hostname": "env.example.test",
});
const serializedAttributes = Object.entries(resolutionAttributes)
.flatMap(([key, value]) => [key, String(value)])
.join("\n");
for (const secret of [
"attacker-user",
"attacker-password",
"/private/workspace",
"attacker-secret",
"attacker-fragment",
"socket-user",
"socket-password",
"/private/socket",
"socket-secret",
"socket-fragment",
]) {
expect(serializedAttributes).not.toContain(secret);
}
expect(requestCount).toBe(0);
}).pipe(
Effect.provide(
connectorTestLayer(execute, {
links: makeLinks({
endpoint: {
httpBaseUrl: "https://attacker.example.test/",
wsBaseUrl: "wss://attacker.example.test/ws",
httpBaseUrl:
"https://attacker-user:attacker-password@attacker.example.test/private/workspace?access_token=attacker-secret#attacker-fragment",
wsBaseUrl:
"wss://socket-user:socket-password@attacker.example.test/private/socket?access_token=socket-secret#socket-fragment",
providerKind: "cloudflare_tunnel",
},
}),
Expand Down Expand Up @@ -700,17 +786,29 @@ describe("EnvironmentConnector", () => {

return Effect.gen(function* () {
const connector = yield* EnvironmentConnector.EnvironmentConnector;
const result = yield* Effect.exit(
const result = yield* Effect.result(
connector.connect({
userId: "user_123",
environmentId: "env-connector-test",
clientProofKeyThumbprint: "client-proof-key-thumbprint",
}),
);

expect(result._tag).toBe("Failure");
if (result._tag === "Failure") {
expect(result.cause.toString()).toContain("EnvironmentMintResponseInvalid");
expect(Result.isFailure(result)).toBe(true);
if (Result.isFailure(result)) {
expect(isEnvironmentMintResponseInvalid(result.failure)).toBe(true);
if (isEnvironmentMintResponseInvalid(result.failure)) {
expect(result.failure).toMatchObject({
userId: "user_123",
environmentId: "env-connector-test",
operation: "connect",
httpBaseUrlInputLength: "https://env.example.test/".length,
httpBaseUrlProtocol: "https:",
httpBaseUrlHostname: "env.example.test",
reason: "proof_verification_failed",
cause: { _tag: "RelayJwtError" },
});
}
}
}).pipe(Effect.provide(connectorTestLayer(execute)));
});
Expand Down Expand Up @@ -780,6 +878,52 @@ describe("EnvironmentConnector", () => {
}).pipe(Effect.provide(connectorTestLayer(execute)));
});

it.effect("preserves context and cause when the mint request fails", () => {
const execute = (request: HttpClientRequest.HttpClientRequest) =>
Effect.succeed(
HttpClientResponse.fromWeb(
request,
Response.json(
{
_tag: "EnvironmentHttpInternalServerError",
message: "Environment is unavailable.",
},
{ status: 500 },
),
),
);

return Effect.gen(function* () {
const connector = yield* EnvironmentConnector.EnvironmentConnector;
const result = yield* Effect.result(
connector.connect({
userId: "user_123",
environmentId: "env-connector-test",
clientProofKeyThumbprint: "client-proof-key-thumbprint",
deviceId: "device-123",
}),
);

expect(Result.isFailure(result)).toBe(true);
if (Result.isFailure(result)) {
expect(isEnvironmentMintRequestFailed(result.failure)).toBe(true);
if (isEnvironmentMintRequestFailed(result.failure)) {
expect(result.failure).toMatchObject({
userId: "user_123",
environmentId: "env-connector-test",
operation: "connect",
stage: "send_request",
httpBaseUrlInputLength: "https://env.example.test/".length,
httpBaseUrlProtocol: "https:",
httpBaseUrlHostname: "env.example.test",
deviceId: "device-123",
cause: { _tag: "EnvironmentHttpInternalServerError" },
});
}
}
}).pipe(Effect.provide(connectorTestLayer(execute)));
});

it.effect("times out hung managed endpoint mint requests", () => {
let resolveRequestStarted: (() => void) | undefined;
const requestStarted = new Promise<void>((resolve) => {
Expand Down Expand Up @@ -810,7 +954,11 @@ describe("EnvironmentConnector", () => {
if (Result.isFailure(result)) {
expect(result.failure._tag).toBe("EnvironmentMintRequestTimedOut");
expect(result.failure).toMatchObject({
userId: "user_123",
environmentId: "env-connector-test",
httpBaseUrlInputLength: "https://env.example.test/".length,
httpBaseUrlProtocol: "https:",
httpBaseUrlHostname: "env.example.test",
timeoutMs: EnvironmentConnector.ENVIRONMENT_MINT_REQUEST_TIMEOUT_MS,
});
}
Expand Down
Loading
Loading