Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
getLogger,
handleError,
isValidGraphName,
isValidGrpcNamingScheme,
isValidLabels,
} from '../../util.js';
import { UnauthorizedError } from '../../errors/errors.js';
Expand Down Expand Up @@ -166,6 +167,19 @@ export function createFederatedSubgraph(
admissionErrors: [],
};
}
// For GRPC_SERVICE subgraphs, validate that routing URL follows gRPC naming scheme
if (req.type === SubgraphType.GRPC_SERVICE && !isValidGrpcNamingScheme(routingUrl)) {
return {
response: {
code: EnumStatusCode.ERR,
details:
`Routing URL must follow gRPC naming scheme. ` +
`See https://grpc.io/docs/guides/custom-name-resolution/ for examples.`,
},
compositionErrors: [],
admissionErrors: [],
};
}
if (req.subscriptionUrl && !isValidUrl(req.subscriptionUrl)) {
return {
response: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
getLogger,
handleError,
isValidGraphName,
isValidGrpcNamingScheme,
isValidLabels,
isValidPluginVersion,
newCompositionOptions,
Expand Down Expand Up @@ -390,6 +391,21 @@ export function publishFederatedSubgraph(
proposalMatchMessage,
};
}
// For GRPC_SERVICE subgraphs, validate that routing URL follows gRPC naming scheme
if (req.type === SubgraphType.GRPC_SERVICE && !isValidGrpcNamingScheme(routingUrl)) {
return {
response: {
code: EnumStatusCode.ERR,
details:
`Routing URL must follow gRPC naming scheme. ` +
`See https://grpc.io/docs/guides/custom-name-resolution/ for examples.`,
},
compositionErrors: [],
deploymentErrors: [],
compositionWarnings: [],
proposalMatchMessage,
};
}

if (req.subscriptionUrl && !isValidUrl(req.subscriptionUrl)) {
return {
Expand Down
26 changes: 25 additions & 1 deletion controlplane/src/core/bufservices/subgraph/updateSubgraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,24 @@ import { PlainMessage } from '@bufbuild/protobuf';
import { HandlerContext } from '@connectrpc/connect';
import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common/common_pb';
import { OrganizationEventName } from '@wundergraph/cosmo-connect/dist/notifications/events_pb';
import { UpdateSubgraphRequest, UpdateSubgraphResponse } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb';
import {
SubgraphType,
UpdateSubgraphRequest,
UpdateSubgraphResponse,
} from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb';
import { isValidUrl } from '@wundergraph/cosmo-shared';
import { AuditLogRepository } from '../../repositories/AuditLogRepository.js';
import { DefaultNamespace } from '../../repositories/NamespaceRepository.js';
import { SubgraphRepository } from '../../repositories/SubgraphRepository.js';
import type { RouterOptions } from '../../routes.js';
import {
enrichLogger,
formatSubgraphType,
formatSubscriptionProtocol,
formatWebsocketSubprotocol,
getLogger,
handleError,
isValidGrpcNamingScheme,
isValidLabels,
newCompositionOptions,
} from '../../util.js';
Expand Down Expand Up @@ -143,6 +149,24 @@ export function updateSubgraph(
compositionWarnings: [],
};
}
// For GRPC_SERVICE subgraphs, validate that routing URL follows gRPC naming scheme
if (
req.routingUrl !== undefined &&
subgraph.type === formatSubgraphType(SubgraphType.GRPC_SERVICE) &&
!isValidGrpcNamingScheme(req.routingUrl)
) {
return {
response: {
code: EnumStatusCode.ERR,
details:
`Routing URL must follow gRPC naming scheme. ` +
`See https://grpc.io/docs/guides/custom-name-resolution/ for examples.`,
},
compositionErrors: [],
deploymentErrors: [],
compositionWarnings: [],
};
}
// When un-setting the url, the url can be an empty string
if (req.subscriptionUrl && !isValidUrl(req.subscriptionUrl)) {
return {
Expand Down
168 changes: 167 additions & 1 deletion controlplane/src/core/util.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { describe, expect, test } from 'vitest';
import { extractOperationNames, hasLabelsChanged, isValidLabels, isValidNamespaceName } from './util.js';
import {
extractOperationNames,
hasLabelsChanged,
isValidGrpcNamingScheme,
isValidLabels,
isValidNamespaceName,
} from './util.js';
import { organizationSlugSchema } from './constants.js';

describe('Util', (ctx) => {
Expand Down Expand Up @@ -276,3 +282,163 @@ describe('extract operation names', () => {
expect(operationNames).toEqual(['getTaskAndUser', 'completedTasks']);
});
});

describe('isValidGrpcNamingScheme', () => {
describe('DNS scheme (default and explicit)', () => {
test('should accept plain hostname (defaults to DNS)', () => {
expect(isValidGrpcNamingScheme('localhost')).toBe(true);
expect(isValidGrpcNamingScheme('example.com')).toBe(true);
expect(isValidGrpcNamingScheme('subdomain.example.com')).toBe(true);
});

test('should accept hostname with port (defaults to DNS)', () => {
expect(isValidGrpcNamingScheme('localhost:8080')).toBe(true);
expect(isValidGrpcNamingScheme('example.com:443')).toBe(true);
expect(isValidGrpcNamingScheme('subdomain.example.com:9090')).toBe(true);
});

test('should accept explicit DNS scheme', () => {
expect(isValidGrpcNamingScheme('dns:localhost')).toBe(true);
expect(isValidGrpcNamingScheme('dns:localhost:8080')).toBe(true);
expect(isValidGrpcNamingScheme('dns:example.com:443')).toBe(true);
expect(isValidGrpcNamingScheme('dns://example.com:8080')).toBe(true);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

test('should accept DNS with authority', () => {
expect(isValidGrpcNamingScheme('dns://8.8.8.8/example.com:8080')).toBe(true);
});
});

describe('Unix domain sockets', () => {
test('should accept unix:path format', () => {
expect(isValidGrpcNamingScheme('unix:/tmp/socket')).toBe(true);
expect(isValidGrpcNamingScheme('unix:./relative/path')).toBe(true);
expect(isValidGrpcNamingScheme('unix:socket')).toBe(true);
});

test('should accept unix:///absolute_path format', () => {
expect(isValidGrpcNamingScheme('unix:///tmp/socket')).toBe(true);
expect(isValidGrpcNamingScheme('unix:///var/run/socket')).toBe(true);
});

test('should reject invalid unix paths', () => {
expect(isValidGrpcNamingScheme('unix:')).toBe(false);
expect(isValidGrpcNamingScheme('unix:///')).toBe(false);
});
});

describe('Unix abstract sockets', () => {
test('should accept unix-abstract:abstract_path', () => {
expect(isValidGrpcNamingScheme('unix-abstract:socket')).toBe(true);
expect(isValidGrpcNamingScheme('unix-abstract:my-socket-name')).toBe(true);
});

test('should reject invalid unix-abstract paths', () => {
expect(isValidGrpcNamingScheme('unix-abstract:')).toBe(false);
});
});

describe('VSOCK', () => {
test('should accept valid vsock:cid:port', () => {
expect(isValidGrpcNamingScheme('vsock:1:8080')).toBe(true);
expect(isValidGrpcNamingScheme('vsock:0:443')).toBe(true);
expect(isValidGrpcNamingScheme('vsock:4294967295:65535')).toBe(true);
});

test('should reject invalid vsock formats', () => {
expect(isValidGrpcNamingScheme('vsock:1')).toBe(false);
expect(isValidGrpcNamingScheme('vsock:1:8080:extra')).toBe(false);
expect(isValidGrpcNamingScheme('vsock:abc:8080')).toBe(false);
expect(isValidGrpcNamingScheme('vsock:1:abc')).toBe(false);
expect(isValidGrpcNamingScheme('vsock:-1:8080')).toBe(false);
expect(isValidGrpcNamingScheme('vsock:1:-1')).toBe(false);
});
});

describe('IPv4 addresses', () => {
test('should accept valid ipv4:address:port', () => {
expect(isValidGrpcNamingScheme('ipv4:127.0.0.1:8080')).toBe(true);
expect(isValidGrpcNamingScheme('ipv4:192.168.1.1:443')).toBe(true);
expect(isValidGrpcNamingScheme('ipv4:0.0.0.0:80')).toBe(true);
expect(isValidGrpcNamingScheme('ipv4:255.255.255.255:65535')).toBe(true);
});

test('should accept ipv4:address without port', () => {
expect(isValidGrpcNamingScheme('ipv4:127.0.0.1')).toBe(true);
expect(isValidGrpcNamingScheme('ipv4:192.168.1.1')).toBe(true);
});

test('should accept multiple IPv4 addresses', () => {
expect(isValidGrpcNamingScheme('ipv4:127.0.0.1:8080,192.168.1.1:9090')).toBe(true);
expect(isValidGrpcNamingScheme('ipv4:127.0.0.1,192.168.1.1:9090')).toBe(true);
});

test('should reject invalid IPv4 addresses', () => {
expect(isValidGrpcNamingScheme('ipv4:256.0.0.1:8080')).toBe(false);
expect(isValidGrpcNamingScheme('ipv4:127.0.0.1.1:8080')).toBe(false);
expect(isValidGrpcNamingScheme('ipv4:localhost:8080')).toBe(false);
expect(isValidGrpcNamingScheme('ipv4:127.0.0.1:-1')).toBe(false);
});
});

describe('IPv6 addresses', () => {
test('should accept valid ipv6:address:port with brackets', () => {
expect(isValidGrpcNamingScheme('ipv6:[::1]:8080')).toBe(true);
expect(isValidGrpcNamingScheme('ipv6:[2001:db8::1]:443')).toBe(true);
expect(isValidGrpcNamingScheme('ipv6:[::]:1234')).toBe(true);
});

test('should accept valid ipv6:address without port', () => {
expect(isValidGrpcNamingScheme('ipv6:[::1]')).toBe(true);
expect(isValidGrpcNamingScheme('ipv6:[2001:db8::1]')).toBe(true);
expect(isValidGrpcNamingScheme('ipv6:::1')).toBe(true);
expect(isValidGrpcNamingScheme('ipv6:2001:db8::1')).toBe(true);
});

test('should accept multiple IPv6 addresses', () => {
expect(isValidGrpcNamingScheme('ipv6:[::1]:8080,[2001:db8::1]:9090')).toBe(true);
});

test('should reject invalid IPv6 addresses', () => {
expect(isValidGrpcNamingScheme('ipv6:[::1]:-1')).toBe(false);
expect(isValidGrpcNamingScheme('ipv6:invalid')).toBe(false);
});
});

describe('Invalid URLs (HTTP/HTTPS)', () => {
test('should reject HTTP URLs', () => {
expect(isValidGrpcNamingScheme('http://localhost:8080')).toBe(false);
expect(isValidGrpcNamingScheme('http://example.com')).toBe(false);
expect(isValidGrpcNamingScheme('http://example.com:8080')).toBe(false);
});

test('should reject HTTPS URLs', () => {
expect(isValidGrpcNamingScheme('https://localhost:8080')).toBe(false);
expect(isValidGrpcNamingScheme('https://example.com')).toBe(false);
expect(isValidGrpcNamingScheme('https://example.com:443')).toBe(false);
});
});

describe('Edge cases', () => {
test('should reject empty strings', () => {
expect(isValidGrpcNamingScheme('')).toBe(false);
expect(isValidGrpcNamingScheme(' ')).toBe(false);
});

test('should handle whitespace', () => {
expect(isValidGrpcNamingScheme(' localhost:8080 ')).toBe(true);
expect(isValidGrpcNamingScheme(' dns:localhost:8080 ')).toBe(true);
});

test('should reject unknown schemes', () => {
expect(isValidGrpcNamingScheme('ftp://example.com')).toBe(false);
expect(isValidGrpcNamingScheme('ws://example.com')).toBe(false);
expect(isValidGrpcNamingScheme('file:///path')).toBe(false);
});

test('should reject malformed URLs', () => {
expect(isValidGrpcNamingScheme('://invalid')).toBe(false);
expect(isValidGrpcNamingScheme('invalid:')).toBe(false);
});
});
});
Loading
Loading