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
29 changes: 29 additions & 0 deletions ingest-v2/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<logback.version>1.4.14</logback.version>
<java.version>11</java.version>
<junit.version>5.10.0</junit.version>
<maven.resources.plugin.version>3.3.1</maven.resources.plugin.version>
<openapi.generator.version>7.15.0</openapi.generator.version>
<slf4j.version>2.0.9</slf4j.version>
<spotless.version>2.46.1</spotless.version>
Expand Down Expand Up @@ -183,6 +184,34 @@
</execution>
</executions>
</plugin>
<!-- Copy WellKnownKustoEndpoints.json from data module to target/classes -->
<!-- This keeps the source tree clean and ensures the file is always from data module -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>${maven.resources.plugin.version}</version>
<executions>
<execution>
<id>copy-well-known-endpoints</id>
<phase>process-resources</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${project.build.outputDirectory}</outputDirectory>
<overwrite>true</overwrite>
<resources>
<resource>
<directory>${project.basedir}/../data/src/main/resources</directory>
<includes>
<include>WellKnownKustoEndpoints.json</include>
</includes>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>kotlin-maven-plugin</artifactId>
<groupId>org.jetbrains.kotlin</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, List<MatchRule>>,
) {
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<MatchRule>): 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<String, MutableList<MatchRule>>()
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<MatchRule>,
): 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)
}
}
Loading
Loading