diff --git a/ingest-v2/pom.xml b/ingest-v2/pom.xml
index f06c611e..767e6208 100644
--- a/ingest-v2/pom.xml
+++ b/ingest-v2/pom.xml
@@ -20,6 +20,7 @@
1.4.14
11
5.10.0
+ 3.3.1
7.15.0
2.0.9
2.46.1
@@ -183,6 +184,34 @@
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ ${maven.resources.plugin.version}
+
+
+ copy-well-known-endpoints
+ process-resources
+
+ copy-resources
+
+
+ ${project.build.outputDirectory}
+ true
+
+
+ ${project.basedir}/../data/src/main/resources
+
+ WellKnownKustoEndpoints.json
+
+
+
+
+
+
+
kotlin-maven-plugin
org.jetbrains.kotlin
diff --git a/ingest-v2/src/main/kotlin/com/microsoft/azure/kusto/ingest/v2/KustoBaseApiClient.kt b/ingest-v2/src/main/kotlin/com/microsoft/azure/kusto/ingest/v2/KustoBaseApiClient.kt
index 5e1830bd..a96f3c94 100644
--- a/ingest-v2/src/main/kotlin/com/microsoft/azure/kusto/ingest/v2/KustoBaseApiClient.kt
+++ b/ingest-v2/src/main/kotlin/com/microsoft/azure/kusto/ingest/v2/KustoBaseApiClient.kt
@@ -5,6 +5,7 @@ package com.microsoft.azure.kusto.ingest.v2
import com.azure.core.credential.TokenCredential
import com.azure.core.credential.TokenRequestContext
import com.microsoft.azure.kusto.ingest.v2.apis.DefaultApi
+import com.microsoft.azure.kusto.ingest.v2.auth.endpoints.KustoTrustedEndpoints
import com.microsoft.azure.kusto.ingest.v2.common.models.ClientDetails
import com.microsoft.azure.kusto.ingest.v2.common.serialization.OffsetDateTimeSerializer
import io.ktor.client.HttpClientConfig
@@ -37,6 +38,15 @@ open class KustoBaseApiClient(
) {
private val logger = LoggerFactory.getLogger(KustoBaseApiClient::class.java)
+ init {
+ // Validate endpoint is trusted unless security checks are skipped
+ // Note: dmUrl might be empty/null in some test scenarios (e.g., mocked clients)
+ // The null check is required for Java interop - Java callers can pass null despite Kotlin's non-null type
+ if (!skipSecurityChecks && dmUrl != null && dmUrl.isNotBlank()) {
+ KustoTrustedEndpoints.validateTrustedEndpoint(dmUrl)
+ }
+ }
+
protected val setupConfig: (HttpClientConfig<*>) -> Unit = { config ->
getClientConfig(config)
}
diff --git a/ingest-v2/src/main/kotlin/com/microsoft/azure/kusto/ingest/v2/auth/endpoints/FastSuffixMatcher.kt b/ingest-v2/src/main/kotlin/com/microsoft/azure/kusto/ingest/v2/auth/endpoints/FastSuffixMatcher.kt
new file mode 100644
index 00000000..efeb6786
--- /dev/null
+++ b/ingest-v2/src/main/kotlin/com/microsoft/azure/kusto/ingest/v2/auth/endpoints/FastSuffixMatcher.kt
@@ -0,0 +1,116 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+package com.microsoft.azure.kusto.ingest.v2.auth.endpoints
+
+/**
+ * Represents a matching rule for endpoint validation.
+ * @param suffix The suffix or hostname to match
+ * @param exact If true, the candidate must exactly match the suffix. If false, candidate must end with the suffix.
+ */
+data class MatchRule(
+ val suffix: String,
+ val exact: Boolean,
+) {
+ val suffixLength: Int
+ get() = suffix.length
+}
+
+/**
+ * Result of a match operation.
+ * @param isMatch Whether the candidate matched
+ * @param matchedRule The rule that matched, or null if no match
+ */
+data class MatchResult(
+ val isMatch: Boolean,
+ val matchedRule: MatchRule?,
+)
+
+/**
+ * A fast suffix matcher that efficiently matches hostnames against a set of rules.
+ * Uses a map indexed by suffix tail for O(1) lookup.
+ */
+class FastSuffixMatcher private constructor(
+ private val suffixLength: Int,
+ private val rules: Map>,
+) {
+ companion object {
+ /**
+ * Creates a new matcher with the provided matching rules.
+ * @param rules One or more matching rules to apply when match is called
+ * @return FastSuffixMatcher
+ */
+ fun create(rules: List): FastSuffixMatcher {
+ require(rules.isNotEmpty()) { "Rules cannot be empty" }
+
+ val minRuleLength = rules.minOfOrNull { it.suffixLength } ?: 0
+ require(minRuleLength > 0) {
+ "Cannot have a match rule whose length is zero"
+ }
+
+ val processedRules = mutableMapOf>()
+ for (rule in rules) {
+ val suffix = rule.suffix.takeLast(minRuleLength).lowercase()
+ processedRules.getOrPut(suffix) { mutableListOf() }.add(rule.copy())
+ }
+
+ return FastSuffixMatcher(minRuleLength, processedRules)
+ }
+
+ /**
+ * Creates a new matcher with the provided matching rules, extending an
+ * existing matcher.
+ * @param existing An existing matcher whose rules are to be baseline
+ * @param rules One or more matching rules to apply when match is called
+ * @return FastSuffixMatcher
+ */
+ fun create(
+ existing: FastSuffixMatcher?,
+ rules: List,
+ ): FastSuffixMatcher {
+ if (existing == null || existing.rules.isEmpty()) {
+ return create(rules)
+ }
+
+ if (rules.isEmpty()) {
+ return existing
+ }
+
+ val combinedRules =
+ rules + existing.rules.values.flatten()
+ return create(combinedRules)
+ }
+ }
+
+ /**
+ * Checks if a candidate string matches any of the rules.
+ * @param candidate A string to match to the list of match rules
+ * @return true if at least one of the rules matched
+ */
+ fun isMatch(candidate: String): Boolean = match(candidate).isMatch
+
+ /**
+ * Matches an input string to the list of match rules.
+ * @param candidate A string to match
+ * @return MatchResult with match status and the matched rule if any
+ */
+ fun match(candidate: String): MatchResult {
+ if (candidate.length < suffixLength) {
+ return MatchResult(false, null)
+ }
+
+ val tail = candidate.takeLast(suffixLength).lowercase()
+ val matchRules = rules[tail]
+
+ if (matchRules != null) {
+ for (rule in matchRules) {
+ if (candidate.endsWith(rule.suffix, ignoreCase = true)) {
+ if (candidate.length == rule.suffix.length || !rule.exact) {
+ return MatchResult(true, rule)
+ }
+ }
+ }
+ }
+
+ return MatchResult(false, null)
+ }
+}
\ No newline at end of file
diff --git a/ingest-v2/src/main/kotlin/com/microsoft/azure/kusto/ingest/v2/auth/endpoints/KustoTrustedEndpoints.kt b/ingest-v2/src/main/kotlin/com/microsoft/azure/kusto/ingest/v2/auth/endpoints/KustoTrustedEndpoints.kt
new file mode 100644
index 00000000..33efe691
--- /dev/null
+++ b/ingest-v2/src/main/kotlin/com/microsoft/azure/kusto/ingest/v2/auth/endpoints/KustoTrustedEndpoints.kt
@@ -0,0 +1,216 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+package com.microsoft.azure.kusto.ingest.v2.auth.endpoints
+
+import com.microsoft.azure.kusto.ingest.v2.exceptions.KustoClientInvalidConnectionStringException
+import org.slf4j.LoggerFactory
+import java.net.URI
+import java.net.URISyntaxException
+
+/**
+ * A helper class to determine which DNS names are "well-known/trusted"
+ * Kusto endpoints. Untrusted endpoints might require additional configuration
+ * before they can be used, for security reasons.
+ */
+object KustoTrustedEndpoints {
+ private val logger = LoggerFactory.getLogger(KustoTrustedEndpoints::class.java)
+
+ /**
+ * Global flag to enable/disable endpoint validation.
+ * When false, untrusted endpoints will only log a warning instead of
+ * throwing an exception.
+ */
+ @JvmField
+ @Volatile
+ var enableWellKnownKustoEndpointsValidation: Boolean = true
+
+ private val matchers: MutableMap = mutableMapOf()
+
+ @Volatile
+ private var additionalMatcher: FastSuffixMatcher? = null
+
+ @Volatile
+ private var overrideMatcher: ((String) -> Boolean)? = null
+
+ // Default login endpoint for public cloud
+ private const val DEFAULT_PUBLIC_LOGIN_ENDPOINT =
+ "https://login.microsoftonline.com"
+
+ init {
+ loadEndpointsFromJson()
+ }
+
+ private fun loadEndpointsFromJson() {
+ try {
+ val endpointsData = WellKnownKustoEndpointsData.getInstance()
+
+ endpointsData.allowedEndpointsByLogin.forEach { (loginEndpoint, allowedEndpoints) ->
+ val rules = mutableListOf()
+
+ // Add suffix rules (exact = false)
+ allowedEndpoints.allowedKustoSuffixes.forEach { suffix ->
+ rules.add(MatchRule(suffix, exact = false))
+ }
+
+ // Add hostname rules (exact = true)
+ allowedEndpoints.allowedKustoHostnames.forEach { hostname ->
+ rules.add(MatchRule(hostname, exact = true))
+ }
+
+ if (rules.isNotEmpty()) {
+ matchers[loginEndpoint.lowercase()] = FastSuffixMatcher.create(rules)
+ }
+ }
+
+ logger.debug(
+ "Loaded {} login endpoint configurations from WellKnownKustoEndpoints.json",
+ matchers.size,
+ )
+ } catch (ex: Exception) {
+ logger.error("Failed to load WellKnownKustoEndpoints.json", ex)
+ throw ex
+ }
+ }
+
+ /**
+ * Sets an override policy for endpoint validation.
+ * @param matcher Rules that determine if a hostname is a valid/trusted
+ * Kusto endpoint (replaces existing rules)
+ */
+ fun setOverridePolicy(matcher: ((String) -> Boolean)?) {
+ overrideMatcher = matcher
+ }
+
+ /**
+ * Adds additional trusted hosts to the matcher.
+ * @param rules A set of rules
+ * @param replace If true, nullifies the last added rules
+ */
+ fun addTrustedHosts(
+ rules: List?,
+ replace: Boolean,
+ ) {
+ if (rules.isNullOrEmpty()) {
+ if (replace) {
+ additionalMatcher = null
+ }
+ return
+ }
+
+ additionalMatcher =
+ FastSuffixMatcher.create(if (replace) null else additionalMatcher, rules)
+ }
+
+ /**
+ * Validates that the endpoint is trusted.
+ * @param uri Kusto endpoint URI string
+ * @param loginEndpoint The login endpoint to check against (optional, defaults to public cloud)
+ * @throws KustoClientInvalidConnectionStringException if endpoint is not trusted
+ */
+ fun validateTrustedEndpoint(
+ uri: String,
+ loginEndpoint: String = DEFAULT_PUBLIC_LOGIN_ENDPOINT,
+ ) {
+ try {
+ validateTrustedEndpoint(URI(uri), loginEndpoint)
+ } catch (ex: URISyntaxException) {
+ throw KustoClientInvalidConnectionStringException(uri, ex.message ?: "Invalid URI", ex)
+ }
+ }
+
+ /**
+ * Validates that the endpoint is trusted.
+ * @param uri Kusto endpoint URI
+ * @param loginEndpoint The login endpoint to check against
+ * @throws KustoClientInvalidConnectionStringException if endpoint is not trusted
+ */
+ fun validateTrustedEndpoint(
+ uri: URI,
+ loginEndpoint: String,
+ ) {
+ val host = uri.host ?: uri.toString()
+ validateHostnameIsTrusted(host, loginEndpoint)
+ }
+
+ /**
+ * Validates that a hostname is trusted.
+ * @param hostname The hostname to validate
+ * @param loginEndpoint The login endpoint to check against
+ * @throws KustoClientInvalidConnectionStringException if hostname is not trusted
+ */
+ private fun validateHostnameIsTrusted(
+ hostname: String,
+ loginEndpoint: String,
+ ) {
+ // Loopback addresses are unconditionally allowed (we trust ourselves)
+ if (isLocalAddress(hostname)) {
+ return
+ }
+
+ // Check override matcher first
+ val override = overrideMatcher
+ if (override != null) {
+ if (override(hostname)) {
+ return
+ }
+ } else {
+ // Check against login-specific matchers
+ val matcher = matchers[loginEndpoint.lowercase()]
+ if (matcher != null && matcher.isMatch(hostname)) {
+ return
+ }
+ }
+
+ // Check additional matchers
+ val additional = additionalMatcher
+ if (additional != null && additional.isMatch(hostname)) {
+ return
+ }
+
+ // Not trusted
+ if (!enableWellKnownKustoEndpointsValidation) {
+ logger.warn(
+ "Can't communicate with '{}' as this hostname is currently not trusted; " +
+ "please see https://aka.ms/kustotrustedendpoints.",
+ hostname,
+ )
+ return
+ }
+
+ throw KustoClientInvalidConnectionStringException(
+ "\$\$ALERT[ValidateHostnameIsTrusted]: Can't communicate with '$hostname' " +
+ "as this hostname is currently not trusted; " +
+ "please see https://aka.ms/kustotrustedendpoints",
+ )
+ }
+
+ /**
+ * Checks if the hostname is a local/loopback address.
+ */
+ private fun isLocalAddress(hostname: String): Boolean {
+ val lowerHost = hostname.lowercase()
+ return lowerHost == "localhost" ||
+ lowerHost == "127.0.0.1" ||
+ lowerHost == "::1" ||
+ lowerHost == "[::1]" ||
+ lowerHost.startsWith("localhost:")
+ }
+
+ /**
+ * Checks if a hostname is trusted without throwing an exception.
+ * @param hostname The hostname to check
+ * @param loginEndpoint The login endpoint to check against
+ * @return true if the hostname is trusted
+ */
+ fun isTrusted(
+ hostname: String,
+ loginEndpoint: String = DEFAULT_PUBLIC_LOGIN_ENDPOINT,
+ ): Boolean {
+ return try {
+ validateHostnameIsTrusted(hostname, loginEndpoint)
+ true
+ } catch (_: KustoClientInvalidConnectionStringException) {
+ false
+ }
+ }
+}
diff --git a/ingest-v2/src/main/kotlin/com/microsoft/azure/kusto/ingest/v2/auth/endpoints/WellKnownKustoEndpointsData.kt b/ingest-v2/src/main/kotlin/com/microsoft/azure/kusto/ingest/v2/auth/endpoints/WellKnownKustoEndpointsData.kt
new file mode 100644
index 00000000..978c7608
--- /dev/null
+++ b/ingest-v2/src/main/kotlin/com/microsoft/azure/kusto/ingest/v2/auth/endpoints/WellKnownKustoEndpointsData.kt
@@ -0,0 +1,59 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+package com.microsoft.azure.kusto.ingest.v2.auth.endpoints
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable as KSerializable
+import kotlinx.serialization.json.Json
+import org.slf4j.LoggerFactory
+
+/**
+ * Data class representing the structure of WellKnownKustoEndpoints.json
+ */
+@KSerializable
+data class AllowedEndpoints(
+ @SerialName("AllowedKustoSuffixes")
+ val allowedKustoSuffixes: List = emptyList(),
+ @SerialName("AllowedKustoHostnames")
+ val allowedKustoHostnames: List = emptyList(),
+)
+
+@KSerializable
+data class WellKnownKustoEndpointsData(
+ @SerialName("_Comments")
+ val comments: List = emptyList(),
+ @SerialName("AllowedEndpointsByLogin")
+ val allowedEndpointsByLogin: Map = emptyMap(),
+) {
+ companion object {
+ private val logger = LoggerFactory.getLogger(WellKnownKustoEndpointsData::class.java)
+
+ @Volatile
+ private var instance: WellKnownKustoEndpointsData? = null
+
+ private val json = Json {
+ ignoreUnknownKeys = true
+ isLenient = true
+ }
+
+ fun getInstance(): WellKnownKustoEndpointsData {
+ return instance ?: synchronized(this) {
+ instance ?: readInstance().also { instance = it }
+ }
+ }
+
+ private fun readInstance(): WellKnownKustoEndpointsData {
+ return try {
+ val resourceStream = WellKnownKustoEndpointsData::class.java
+ .getResourceAsStream("/WellKnownKustoEndpoints.json")
+ ?: throw RuntimeException("WellKnownKustoEndpoints.json not found in classpath")
+
+ val content = resourceStream.bufferedReader().use { it.readText() }
+ json.decodeFromString(content)
+ } catch (ex: Exception) {
+ logger.error("Failed to read WellKnownKustoEndpoints.json", ex)
+ throw RuntimeException("Failed to read WellKnownKustoEndpoints.json", ex)
+ }
+ }
+ }
+}
diff --git a/ingest-v2/src/main/kotlin/com/microsoft/azure/kusto/ingest/v2/exceptions/KustoClientInvalidConnectionStringException.kt b/ingest-v2/src/main/kotlin/com/microsoft/azure/kusto/ingest/v2/exceptions/KustoClientInvalidConnectionStringException.kt
new file mode 100644
index 00000000..7bda6f3f
--- /dev/null
+++ b/ingest-v2/src/main/kotlin/com/microsoft/azure/kusto/ingest/v2/exceptions/KustoClientInvalidConnectionStringException.kt
@@ -0,0 +1,17 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+package com.microsoft.azure.kusto.ingest.v2.exceptions
+
+/**
+ * Exception thrown when a connection string is invalid or the endpoint is not
+ * trusted.
+ */
+class KustoClientInvalidConnectionStringException : RuntimeException {
+ constructor(message: String) : super(message)
+
+ constructor(
+ uri: String,
+ message: String,
+ cause: Throwable,
+ ) : super("Invalid connection string for URI '$uri': $message", cause)
+}
diff --git a/ingest-v2/src/test/kotlin/com/microsoft/azure/kusto/ingest/v2/TrustedEndpointValidationTest.kt b/ingest-v2/src/test/kotlin/com/microsoft/azure/kusto/ingest/v2/TrustedEndpointValidationTest.kt
new file mode 100644
index 00000000..e2b1c489
--- /dev/null
+++ b/ingest-v2/src/test/kotlin/com/microsoft/azure/kusto/ingest/v2/TrustedEndpointValidationTest.kt
@@ -0,0 +1,272 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+package com.microsoft.azure.kusto.ingest.v2
+
+import com.azure.core.credential.AccessToken
+import com.azure.core.credential.TokenCredential
+import com.azure.core.credential.TokenRequestContext
+import com.microsoft.azure.kusto.ingest.v2.auth.endpoints.KustoTrustedEndpoints
+import com.microsoft.azure.kusto.ingest.v2.builders.QueuedIngestClientBuilder
+import com.microsoft.azure.kusto.ingest.v2.builders.StreamingIngestClientBuilder
+import com.microsoft.azure.kusto.ingest.v2.exceptions.KustoClientInvalidConnectionStringException
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertDoesNotThrow
+import org.junit.jupiter.api.assertThrows
+import org.junit.jupiter.api.parallel.Execution
+import org.junit.jupiter.api.parallel.ExecutionMode
+import reactor.core.publisher.Mono
+import java.time.OffsetDateTime
+import kotlin.test.assertTrue
+
+/**
+ * Tests for endpoint validation functionality in ingest-v2.
+ *
+ * These tests verify that:
+ * 1. Adhoc/untrusted Kusto endpoints are blocked by default
+ * 2. The skipSecurityChecks() method allows untrusted endpoints
+ * 3. Trusted Kusto endpoints are allowed by default
+ *
+ * The validation is now implemented natively in ingest-v2 using:
+ * - WellKnownKustoEndpoints.json (copied from data module during build)
+ * - KustoTrustedEndpoints object for validation logic
+ * - KustoClientInvalidConnectionStringException for untrusted endpoints
+ *
+ * Note: This test class uses SAME_THREAD execution mode to prevent race conditions
+ * when modifying the global enableWellKnownKustoEndpointsValidation flag.
+ */
+@Execution(ExecutionMode.SAME_THREAD)
+class TrustedEndpointValidationTest {
+
+ // Mock token credential for testing
+ private val mockTokenCredential = TokenCredential { _: TokenRequestContext ->
+ Mono.just(AccessToken("mock-token", OffsetDateTime.now().plusHours(1)))
+ }
+
+ // Example of an adhoc/untrusted endpoint
+ private val untrustedEndpoint = "https://my-random-adhoc-cluster.example.com"
+
+ // Example of a trusted Kusto endpoint (public cloud)
+ private val trustedEndpoint = "https://mycluster.kusto.windows.net"
+
+ // Store original validation state to restore after tests
+ private var originalValidationState: Boolean = true
+
+ @BeforeEach
+ fun setUp() {
+ // Save original state and ensure validation is enabled
+ originalValidationState =
+ KustoTrustedEndpoints.enableWellKnownKustoEndpointsValidation
+ KustoTrustedEndpoints.enableWellKnownKustoEndpointsValidation = true
+ }
+
+ @AfterEach
+ fun tearDown() {
+ // Restore original validation state
+ KustoTrustedEndpoints.enableWellKnownKustoEndpointsValidation =
+ originalValidationState
+ }
+
+ // ============================================================================
+ // UNTRUSTED ENDPOINT TESTS - Should throw exception
+ // ============================================================================
+
+ @Test
+ @DisplayName("StreamingIngestClient: Untrusted endpoint throws exception without skipSecurityChecks")
+ fun `streaming client - untrusted endpoint throws without skip security checks`() {
+ val exception =
+ assertThrows {
+ StreamingIngestClientBuilder.create(untrustedEndpoint)
+ .withAuthentication(mockTokenCredential)
+ // Note: NOT calling skipSecurityChecks()
+ .build()
+ }
+
+ assertTrue(
+ exception.message?.contains("not trusted") == true ||
+ exception.message?.contains("kustotrustedendpoints") == true,
+ "Exception should indicate endpoint is not trusted. Actual: ${exception.message}",
+ )
+ }
+
+ @Test
+ @DisplayName("QueuedIngestClient: Untrusted endpoint throws exception without skipSecurityChecks")
+ fun `queued client - untrusted endpoint throws without skip security checks`() {
+ val exception =
+ assertThrows {
+ QueuedIngestClientBuilder.create(untrustedEndpoint)
+ .withAuthentication(mockTokenCredential)
+ // Note: NOT calling skipSecurityChecks()
+ .build()
+ }
+
+ assertTrue(
+ exception.message?.contains("not trusted") == true ||
+ exception.message?.contains("kustotrustedendpoints") == true,
+ "Exception should indicate endpoint is not trusted. Actual: ${exception.message}",
+ )
+ }
+
+ // ============================================================================
+ // SKIP SECURITY CHECKS TESTS - Should work with the flag
+ // ============================================================================
+
+ @Test
+ @DisplayName("StreamingIngestClient: Untrusted endpoint works with skipSecurityChecks")
+ fun `streaming client - untrusted endpoint works with skip security checks`() {
+ assertDoesNotThrow {
+ StreamingIngestClientBuilder.create(untrustedEndpoint)
+ .withAuthentication(mockTokenCredential)
+ .skipSecurityChecks()
+ .build()
+ }
+ }
+
+ @Test
+ @DisplayName("QueuedIngestClient: Untrusted endpoint works with skipSecurityChecks")
+ fun `queued client - untrusted endpoint works with skip security checks`() {
+ assertDoesNotThrow {
+ QueuedIngestClientBuilder.create(untrustedEndpoint)
+ .withAuthentication(mockTokenCredential)
+ .skipSecurityChecks()
+ .build()
+ }
+ }
+
+ // ============================================================================
+ // TRUSTED ENDPOINT TESTS - Should work without skipSecurityChecks
+ // ============================================================================
+
+ @Test
+ @DisplayName("StreamingIngestClient: Trusted Kusto endpoint works without skipSecurityChecks")
+ fun `streaming client - trusted endpoint works without skip security checks`() {
+ assertDoesNotThrow {
+ StreamingIngestClientBuilder.create(trustedEndpoint)
+ .withAuthentication(mockTokenCredential)
+ .build()
+ }
+ }
+
+ @Test
+ @DisplayName("QueuedIngestClient: Trusted Kusto endpoint works without skipSecurityChecks")
+ fun `queued client - trusted endpoint works without skip security checks`() {
+ assertDoesNotThrow {
+ QueuedIngestClientBuilder.create(trustedEndpoint)
+ .withAuthentication(mockTokenCredential)
+ .build()
+ }
+ }
+
+ // ============================================================================
+ // GLOBAL FLAG TESTS
+ // ============================================================================
+
+ @Test
+ @DisplayName("Global validation flag can disable endpoint checks")
+ fun `global validation flag disables endpoint checks`() {
+ // Disable validation globally
+ KustoTrustedEndpoints.enableWellKnownKustoEndpointsValidation = false
+
+ try {
+ // Now untrusted endpoints should work even without skipSecurityChecks
+ assertDoesNotThrow {
+ StreamingIngestClientBuilder.create(untrustedEndpoint)
+ .withAuthentication(mockTokenCredential)
+ .build()
+ }
+ } finally {
+ // Re-enable for other tests
+ KustoTrustedEndpoints.enableWellKnownKustoEndpointsValidation = true
+ }
+ }
+
+ // ============================================================================
+ // CLOUD-SPECIFIC ENDPOINT TESTS
+ // ============================================================================
+
+ @Test
+ @DisplayName("Various cloud-specific endpoints are trusted")
+ fun `cloud specific endpoints are trusted`() {
+ val trustedEndpoints =
+ listOf(
+ // Public cloud
+ "https://mycluster.kusto.windows.net",
+ "https://mycluster.kustodev.windows.net",
+ "https://mycluster.kustomfa.windows.net",
+ // Fabric
+ "https://mycluster.kusto.fabric.microsoft.com",
+ // Synapse
+ "https://mycluster.kusto.azuresynapse.net",
+ )
+
+ trustedEndpoints.forEach { endpoint ->
+ assertDoesNotThrow("Endpoint $endpoint should be trusted") {
+ StreamingIngestClientBuilder.create(endpoint)
+ .withAuthentication(mockTokenCredential)
+ .build()
+ }
+ }
+ }
+
+ @Test
+ @DisplayName("Localhost endpoints are always trusted")
+ fun `localhost endpoints are trusted`() {
+ val localhostEndpoints =
+ listOf(
+ "https://localhost:8080",
+ "https://127.0.0.1:8080",
+ "https://localhost",
+ )
+
+ localhostEndpoints.forEach { endpoint ->
+ assertDoesNotThrow("Localhost endpoint $endpoint should be trusted") {
+ StreamingIngestClientBuilder.create(endpoint)
+ .withAuthentication(mockTokenCredential)
+ .build()
+ }
+ }
+ }
+
+ // ============================================================================
+ // DIRECT API TESTS - Test KustoTrustedEndpoints directly
+ // ============================================================================
+
+ @Test
+ @DisplayName("KustoTrustedEndpoints.isTrusted returns correct values")
+ fun `isTrusted returns correct values`() {
+ assertTrue(
+ KustoTrustedEndpoints.isTrusted("mycluster.kusto.windows.net"),
+ "Public cloud endpoint should be trusted",
+ )
+ assertTrue(
+ KustoTrustedEndpoints.isTrusted("mycluster.kusto.fabric.microsoft.com"),
+ "Fabric endpoint should be trusted",
+ )
+ assertTrue(
+ KustoTrustedEndpoints.isTrusted("localhost"),
+ "Localhost should be trusted",
+ )
+ assertTrue(
+ !KustoTrustedEndpoints.isTrusted("random.example.com"),
+ "Random endpoint should not be trusted",
+ )
+ }
+
+ @Test
+ @DisplayName("KustoTrustedEndpoints.validateTrustedEndpoint throws for untrusted")
+ fun `validateTrustedEndpoint throws for untrusted endpoints`() {
+ assertThrows {
+ KustoTrustedEndpoints.validateTrustedEndpoint("https://evil.example.com")
+ }
+ }
+
+ @Test
+ @DisplayName("KustoTrustedEndpoints.validateTrustedEndpoint passes for trusted")
+ fun `validateTrustedEndpoint passes for trusted endpoints`() {
+ assertDoesNotThrow {
+ KustoTrustedEndpoints.validateTrustedEndpoint("https://mycluster.kusto.windows.net")
+ }
+ }
+}
\ No newline at end of file