diff --git a/core/Azure.Mcp.Core/src/Areas/Subscription/Commands/SubscriptionListCommand.cs b/core/Azure.Mcp.Core/src/Areas/Subscription/Commands/SubscriptionListCommand.cs index 6100e943bb..0805fbe319 100644 --- a/core/Azure.Mcp.Core/src/Areas/Subscription/Commands/SubscriptionListCommand.cs +++ b/core/Azure.Mcp.Core/src/Areas/Subscription/Commands/SubscriptionListCommand.cs @@ -7,6 +7,7 @@ using Azure.ResourceManager.Resources; using Microsoft.Extensions.Logging; using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Helpers; using Microsoft.Mcp.Core.Models.Command; namespace Azure.Mcp.Core.Areas.Subscription.Commands; @@ -65,7 +66,7 @@ internal static List MapToSubscriptionInfos(List> GetResourceGroups(string subscription var cachedResults = await _cacheService.GetAsync>(CacheGroup, cacheKey, s_cacheDuration, cancellationToken); if (cachedResults != null) { - return cachedResults.FirstOrDefault(rg => rg.Name.Equals(resourceGroupName, StringComparison.OrdinalIgnoreCase)); + return cachedResults.FirstOrDefault(rg => rg.Name.Equals(resourceGroupName, StringComparisons.ResourceGroup)); } var rg = await GetResourceGroupResource(subscription, resourceGroupName, tenant, retryPolicy, cancellationToken); diff --git a/core/Azure.Mcp.Core/src/Services/Azure/Subscription/SubscriptionService.cs b/core/Azure.Mcp.Core/src/Services/Azure/Subscription/SubscriptionService.cs index 99d98453d5..5ef5cd3c34 100644 --- a/core/Azure.Mcp.Core/src/Services/Azure/Subscription/SubscriptionService.cs +++ b/core/Azure.Mcp.Core/src/Services/Azure/Subscription/SubscriptionService.cs @@ -94,7 +94,7 @@ public bool IsSubscriptionId(string subscription, string? tenant = null) public async Task GetSubscriptionIdByName(string subscriptionName, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default) { var subscriptions = await GetSubscriptions(tenant, retryPolicy, cancellationToken); - var subscription = subscriptions.FirstOrDefault(s => s.DisplayName.Equals(subscriptionName, StringComparison.OrdinalIgnoreCase)) ?? + var subscription = subscriptions.FirstOrDefault(s => s.DisplayName.Equals(subscriptionName, StringComparisons.SubscriptionDisplayName)) ?? throw new KeyNotFoundException($"Could not find subscription with name {subscriptionName}"); return subscription.SubscriptionId; @@ -103,7 +103,7 @@ public async Task GetSubscriptionIdByName(string subscriptionName, strin public async Task GetSubscriptionNameById(string subscriptionId, string? tenant = null, RetryPolicyOptions? retryPolicy = null, CancellationToken cancellationToken = default) { var subscriptions = await GetSubscriptions(tenant, retryPolicy, cancellationToken); - var subscription = subscriptions.FirstOrDefault(s => s.SubscriptionId.Equals(subscriptionId, StringComparison.OrdinalIgnoreCase)) ?? + var subscription = subscriptions.FirstOrDefault(s => s.SubscriptionId.Equals(subscriptionId, StringComparisons.SubscriptionId)) ?? throw new KeyNotFoundException($"Could not find subscription with ID {subscriptionId}"); return subscription.DisplayName; diff --git a/core/Azure.Mcp.Core/src/Services/Azure/Tenant/TenantService.cs b/core/Azure.Mcp.Core/src/Services/Azure/Tenant/TenantService.cs index ba7ff62782..a50affa3ed 100644 --- a/core/Azure.Mcp.Core/src/Services/Azure/Tenant/TenantService.cs +++ b/core/Azure.Mcp.Core/src/Services/Azure/Tenant/TenantService.cs @@ -6,6 +6,7 @@ using Azure.ResourceManager; using Azure.ResourceManager.Resources; using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Helpers; using Microsoft.Mcp.Core.Services.Azure.Authentication; using Microsoft.Mcp.Core.Services.Caching; @@ -107,7 +108,7 @@ public async Task GetTenantIdByName(string tenantName, CancellationToken public async Task GetTenantNameById(string tenantId, CancellationToken cancellationToken) { var tenants = await GetTenants(cancellationToken); - var tenant = tenants.FirstOrDefault(t => t.Data.TenantId?.ToString().Equals(tenantId, StringComparison.OrdinalIgnoreCase) == true) ?? + var tenant = tenants.FirstOrDefault(t => t.Data.TenantId?.ToString().Equals(tenantId, StringComparisons.TenantId) == true) ?? throw new KeyNotFoundException($"Could not find tenant with ID {tenantId}"); string? tenantName = tenant.Data.DisplayName ?? diff --git a/core/Microsoft.Mcp.Core/src/Helpers/StringComparisons.cs b/core/Microsoft.Mcp.Core/src/Helpers/StringComparisons.cs new file mode 100644 index 0000000000..016b258552 --- /dev/null +++ b/core/Microsoft.Mcp.Core/src/Helpers/StringComparisons.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.Mcp.Core.Helpers; + +/// +/// Centralizes the values to use when comparing +/// well-known string values, making the intended comparison semantics explicit +/// at each call site. +/// +public static class StringComparisons +{ + /// + /// The comparison to use when comparing Azure tenant IDs. Tenant IDs are + /// GUIDs whose casing is not significant, so they are compared + /// case-insensitively. + /// + public static StringComparison TenantId => StringComparison.OrdinalIgnoreCase; + + /// + /// The comparison to use when comparing Azure subscription IDs. Subscription + /// IDs are GUIDs whose casing is not significant, so they are compared + /// case-insensitively. + /// + public static StringComparison SubscriptionId => StringComparison.OrdinalIgnoreCase; + + /// + /// The comparison to use when comparing Azure subscription display names, + /// which are matched case-insensitively. + /// + public static StringComparison SubscriptionDisplayName => StringComparison.OrdinalIgnoreCase; + + /// + /// The comparison to use when comparing Azure resource group names, which + /// Azure Resource Manager treats case-insensitively. + /// + public static StringComparison ResourceGroup => StringComparison.OrdinalIgnoreCase; + + /// + /// The comparison to use when matching an Azure resource by its name. Azure + /// resource names are matched case-insensitively for lookup purposes. + /// + public static StringComparison ResourceName => StringComparison.OrdinalIgnoreCase; +} diff --git a/core/Microsoft.Mcp.Core/src/Services/Azure/Authentication/HttpOnBehalfOfTokenCredentialProvider.cs b/core/Microsoft.Mcp.Core/src/Services/Azure/Authentication/HttpOnBehalfOfTokenCredentialProvider.cs index 815dba6bff..0117dcee34 100644 --- a/core/Microsoft.Mcp.Core/src/Services/Azure/Authentication/HttpOnBehalfOfTokenCredentialProvider.cs +++ b/core/Microsoft.Mcp.Core/src/Services/Azure/Authentication/HttpOnBehalfOfTokenCredentialProvider.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Identity.Web; +using Microsoft.Mcp.Core.Helpers; namespace Microsoft.Mcp.Core.Services.Azure.Authentication; @@ -33,7 +34,7 @@ public Task GetTokenCredentialAsync(string? tenantId, Cancellat if (tenantId is not null) { if (httpContext.User.FindFirst("tid")?.Value is string tidClaim - && tidClaim != tenantId) + && !string.Equals(tidClaim, tenantId, StringComparisons.TenantId)) { _logger.LogWarning( "The requested token tenant '{GetTokenTenant}' does not match the tenant of the authenticated user '{TidClaim}'. Going to throw.", diff --git a/servers/Azure.Mcp.Server/changelog-entries/tmeschter-fix-obo-tenant-casing.yaml b/servers/Azure.Mcp.Server/changelog-entries/tmeschter-fix-obo-tenant-casing.yaml new file mode 100644 index 0000000000..fb2de894e8 --- /dev/null +++ b/servers/Azure.Mcp.Server/changelog-entries/tmeschter-fix-obo-tenant-casing.yaml @@ -0,0 +1,4 @@ +pr: 2907 +changes: + - section: "Bugs Fixed" + description: "Fixed an issue where the on-behalf-of token tenant comparison was case-sensitive, causing valid requests to be rejected when the configured tenant ID casing differed from the JWT 'tid' claim." diff --git a/tools/Azure.Mcp.Tools.AppLens/src/Services/AppLensService.cs b/tools/Azure.Mcp.Tools.AppLens/src/Services/AppLensService.cs index 554521c1e1..6f2a789095 100644 --- a/tools/Azure.Mcp.Tools.AppLens/src/Services/AppLensService.cs +++ b/tools/Azure.Mcp.Tools.AppLens/src/Services/AppLensService.cs @@ -16,6 +16,7 @@ using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Helpers; using Microsoft.Mcp.Core.Services.Azure.Authentication; namespace Azure.Mcp.Tools.AppLens.Services; @@ -107,7 +108,7 @@ internal async Task FindResourceAsync( if (!string.IsNullOrEmpty(resourceGroup)) { var rgFiltered = filteredResults - .Where(r => r.ResourceGroup.Equals(resourceGroup, StringComparison.OrdinalIgnoreCase)) + .Where(r => r.ResourceGroup.Equals(resourceGroup, StringComparisons.ResourceGroup)) .ToImmutableArray(); if (rgFiltered.Length == 0) diff --git a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/AzureBackupService.cs b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/AzureBackupService.cs index c9aba94420..40c4b105fe 100644 --- a/tools/Azure.Mcp.Tools.AzureBackup/src/Services/AzureBackupService.cs +++ b/tools/Azure.Mcp.Tools.AzureBackup/src/Services/AzureBackupService.cs @@ -12,6 +12,7 @@ using Azure.ResourceManager.RecoveryServicesBackup.Models; using Azure.ResourceManager.Resources; using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Helpers; using Microsoft.Mcp.Core.Options; using SdkBackupStatusResult = Azure.ResourceManager.RecoveryServicesBackup.Models.BackupStatusResult; @@ -135,7 +136,7 @@ public async Task> ListVaultsAsync( List FilterByResourceGroup(List vaults) => string.IsNullOrEmpty(resourceGroup) ? vaults - : vaults.Where(v => string.Equals(v.ResourceGroup, resourceGroup, StringComparison.OrdinalIgnoreCase)).ToList(); + : vaults.Where(v => string.Equals(v.ResourceGroup, resourceGroup, StringComparisons.ResourceGroup)).ToList(); if (VaultTypeResolver.IsRsv(vaultType)) { @@ -658,7 +659,7 @@ public async Task> FindUnprotectedResourcesAsync( // Apply optional resource group filter if (!string.IsNullOrEmpty(resourceGroup) && - !string.Equals(resource.Id?.ResourceGroupName, resourceGroup, StringComparison.OrdinalIgnoreCase)) + !string.Equals(resource.Id?.ResourceGroupName, resourceGroup, StringComparisons.ResourceGroup)) { continue; } diff --git a/tools/Azure.Mcp.Tools.AzureIsv/tests/Azure.Mcp.Tools.AzureIsv.Tests/AzureIsvCommandTests.cs b/tools/Azure.Mcp.Tools.AzureIsv/tests/Azure.Mcp.Tools.AzureIsv.Tests/AzureIsvCommandTests.cs index a8fec6538c..775984dcb2 100644 --- a/tools/Azure.Mcp.Tools.AzureIsv/tests/Azure.Mcp.Tools.AzureIsv.Tests/AzureIsvCommandTests.cs +++ b/tools/Azure.Mcp.Tools.AzureIsv/tests/Azure.Mcp.Tools.AzureIsv.Tests/AzureIsvCommandTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System.Text.Json; +using Microsoft.Mcp.Core.Helpers; using Microsoft.Mcp.Tests; using Microsoft.Mcp.Tests.Client; using Microsoft.Mcp.Tests.Client.Helpers; @@ -17,7 +18,7 @@ public class AzureIsvCommandTests(ITestOutputHelper output, TestProxyFixture fix public async Task Should_list_datadog_monitored_resources() { // Skipping test if Tenant is not 'Customer LED Tenant' - if (Settings.TenantId != "888d76fa-54b2-4ced-8ee5-aac1585adee7" && Settings.TestMode != TestMode.Playback) + if (!string.Equals(Settings.TenantId, "888d76fa-54b2-4ced-8ee5-aac1585adee7", StringComparisons.TenantId) && Settings.TestMode != TestMode.Playback) { Assert.Skip("Test skipped because Tenant is not 'Customer LED Tenant'."); } diff --git a/tools/Azure.Mcp.Tools.Compute/src/Commands/Disk/DiskUpdateCommand.cs b/tools/Azure.Mcp.Tools.Compute/src/Commands/Disk/DiskUpdateCommand.cs index 6efc16ba9b..6d014410f8 100644 --- a/tools/Azure.Mcp.Tools.Compute/src/Commands/Disk/DiskUpdateCommand.cs +++ b/tools/Azure.Mcp.Tools.Compute/src/Commands/Disk/DiskUpdateCommand.cs @@ -8,6 +8,7 @@ using Azure.Mcp.Tools.Compute.Services; using Microsoft.Mcp.Core.Commands; using Microsoft.Mcp.Core.Extensions; +using Microsoft.Mcp.Core.Helpers; using Microsoft.Mcp.Core.Models.Command; using Microsoft.Mcp.Core.Models.Option; @@ -132,7 +133,7 @@ public override async Task ExecuteAsync(CommandContext context, cancellationToken); var matchingDisks = disks - .Where(d => string.Equals(d.Name, options.Disk, StringComparison.OrdinalIgnoreCase)) + .Where(d => string.Equals(d.Name, options.Disk, StringComparisons.ResourceName)) .ToList(); if (matchingDisks.Count == 0) diff --git a/tools/Azure.Mcp.Tools.Cosmos/src/Services/CosmosService.cs b/tools/Azure.Mcp.Tools.Cosmos/src/Services/CosmosService.cs index 760b44ba42..63d0cf78f2 100644 --- a/tools/Azure.Mcp.Tools.Cosmos/src/Services/CosmosService.cs +++ b/tools/Azure.Mcp.Tools.Cosmos/src/Services/CosmosService.cs @@ -78,7 +78,7 @@ private async Task GetCosmosAccountAsync( await foreach (var account in subscriptionResource.GetCosmosDBAccountsAsync(cancellationToken)) { // Cosmos DB account names are case-insensitive in Azure, so compare accordingly. - if (account.Data.Name.Equals(accountName, StringComparison.OrdinalIgnoreCase)) + if (account.Data.Name.Equals(accountName, StringComparisons.ResourceName)) { return account; } diff --git a/tools/Azure.Mcp.Tools.EventGrid/src/Services/EventGridService.cs b/tools/Azure.Mcp.Tools.EventGrid/src/Services/EventGridService.cs index 2fb8f80828..aebedff647 100644 --- a/tools/Azure.Mcp.Tools.EventGrid/src/Services/EventGridService.cs +++ b/tools/Azure.Mcp.Tools.EventGrid/src/Services/EventGridService.cs @@ -8,6 +8,7 @@ using Azure.ResourceManager.EventGrid; using Azure.ResourceManager.EventGrid.Models; using Azure.ResourceManager.Resources; +using Microsoft.Mcp.Core.Helpers; using Microsoft.Mcp.Core.Options; namespace Azure.Mcp.Tools.EventGrid.Services; @@ -374,7 +375,7 @@ private async Task GetSubscriptionsFromAllTopics( await foreach (var topic in resourceGroupResource.Value.GetEventGridTopics().GetAllAsync(cancellationToken: cancellationToken)) { - if (topic.Data.Name.Equals(topicName, StringComparison.OrdinalIgnoreCase)) + if (topic.Data.Name.Equals(topicName, StringComparisons.ResourceName)) { return topic; } @@ -385,7 +386,7 @@ private async Task GetSubscriptionsFromAllTopics( // Search in all resource groups await foreach (var topic in subscriptionResource.GetEventGridTopicsAsync(cancellationToken: cancellationToken)) { - if (topic.Data.Name.Equals(topicName, StringComparison.OrdinalIgnoreCase)) + if (topic.Data.Name.Equals(topicName, StringComparisons.ResourceName)) { return topic; } @@ -408,7 +409,7 @@ private async Task GetSubscriptionsFromAllTopics( await foreach (var systemTopic in resourceGroupResource.Value.GetSystemTopics().GetAllAsync(cancellationToken: cancellationToken)) { - if (systemTopic.Data.Name.Equals(topicName, StringComparison.OrdinalIgnoreCase)) + if (systemTopic.Data.Name.Equals(topicName, StringComparisons.ResourceName)) { return systemTopic; } @@ -419,7 +420,7 @@ private async Task GetSubscriptionsFromAllTopics( // Search in all resource groups await foreach (var systemTopic in subscriptionResource.GetSystemTopicsAsync(cancellationToken: cancellationToken)) { - if (systemTopic.Data.Name.Equals(topicName, StringComparison.OrdinalIgnoreCase)) + if (systemTopic.Data.Name.Equals(topicName, StringComparisons.ResourceName)) { return systemTopic; } diff --git a/tools/Azure.Mcp.Tools.FileShares/src/Services/FileSharesService.cs b/tools/Azure.Mcp.Tools.FileShares/src/Services/FileSharesService.cs index ada27ab29e..f33f3f0e70 100644 --- a/tools/Azure.Mcp.Tools.FileShares/src/Services/FileSharesService.cs +++ b/tools/Azure.Mcp.Tools.FileShares/src/Services/FileSharesService.cs @@ -4,6 +4,7 @@ using System.Net; using Azure.ResourceManager.FileShares; using Azure.ResourceManager.Resources; +using Microsoft.Mcp.Core.Helpers; using Microsoft.Mcp.Core.Options; namespace Azure.Mcp.Tools.FileShares.Services; @@ -435,7 +436,7 @@ public async Task GetSnapshotAsync( await foreach (var snapshotResource in snapshotCollection.WithCancellation(cancellationToken)) { - if (snapshotResource.Data.Name.Equals(snapshotId, StringComparison.OrdinalIgnoreCase) || + if (snapshotResource.Data.Name.Equals(snapshotId, StringComparisons.ResourceName) || snapshotResource.Data.Id.ToString().Equals(snapshotId, StringComparison.OrdinalIgnoreCase)) { return FileShareSnapshotInfo.FromResource(snapshotResource); diff --git a/tools/Azure.Mcp.Tools.FoundryExtensions/src/Services/FoundryExtensionsService.cs b/tools/Azure.Mcp.Tools.FoundryExtensions/src/Services/FoundryExtensionsService.cs index b11366c77c..593286726b 100644 --- a/tools/Azure.Mcp.Tools.FoundryExtensions/src/Services/FoundryExtensionsService.cs +++ b/tools/Azure.Mcp.Tools.FoundryExtensions/src/Services/FoundryExtensionsService.cs @@ -173,7 +173,7 @@ public async Task GetKnowledgeIndexSchema( // Find the index by name using async enumerable var index = await indexesClient.GetIndicesAsync(cancellationToken: cancellationToken) - .Where(i => string.Equals(i.Name, indexName, StringComparison.OrdinalIgnoreCase)) + .Where(i => string.Equals(i.Name, indexName, StringComparisons.ResourceName)) .FirstOrDefaultAsync(cancellationToken: cancellationToken); if (index == null) @@ -648,7 +648,7 @@ public async Task> ListAiResourcesAsync( { var resourceInfo = await BuildResourceInformation(account, subscriptionResource.Data.DisplayName, cancellationToken); resources.Add(resourceInfo); - if (account.Data.Id.ResourceGroupName?.Equals(resourceGroup, StringComparison.OrdinalIgnoreCase) == true) + if (account.Data.Id.ResourceGroupName?.Equals(resourceGroup, StringComparisons.ResourceGroup) == true) { var retrieved = await BuildResourceInformation(account, subscriptionResource.Data.DisplayName, cancellationToken); resources.Add(retrieved); diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Services/MonitorService.cs b/tools/Azure.Mcp.Tools.Monitor/src/Services/MonitorService.cs index e211c5db38..f4a02614f7 100644 --- a/tools/Azure.Mcp.Tools.Monitor/src/Services/MonitorService.cs +++ b/tools/Azure.Mcp.Tools.Monitor/src/Services/MonitorService.cs @@ -492,7 +492,7 @@ private async Task MakeActivityLogRequestAsync(string u // Find the workspace var matchingWorkspace = workspaces.FirstOrDefault(w => isId ? w.CustomerId.Equals(workspace, StringComparison.OrdinalIgnoreCase) - : w.Name.Equals(workspace, StringComparison.OrdinalIgnoreCase)); + : w.Name.Equals(workspace, StringComparisons.ResourceName)); if (matchingWorkspace == null) { diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Services/ResourceResolverService.cs b/tools/Azure.Mcp.Tools.Monitor/src/Services/ResourceResolverService.cs index 65d4f20d81..34b52f3bca 100644 --- a/tools/Azure.Mcp.Tools.Monitor/src/Services/ResourceResolverService.cs +++ b/tools/Azure.Mcp.Tools.Monitor/src/Services/ResourceResolverService.cs @@ -5,6 +5,7 @@ using Azure.Mcp.Core.Services.Azure; using Azure.Mcp.Core.Services.Azure.Subscription; using Azure.Mcp.Core.Services.Azure.Tenant; +using Microsoft.Mcp.Core.Helpers; using Microsoft.Mcp.Core.Options; namespace Azure.Mcp.Tools.Monitor.Services; @@ -42,7 +43,7 @@ public async Task ResolveResourceIdAsync( // Get all resources matching the name var allMatchingResources = await subscriptionResource.GetGenericResourcesAsync(cancellationToken: cancellationToken) - .Where(r => r.Data.Name?.Equals(resourceName, StringComparison.OrdinalIgnoreCase) == true) + .Where(r => r.Data.Name?.Equals(resourceName, StringComparisons.ResourceName) == true) .ToListAsync(cancellationToken: cancellationToken); if (allMatchingResources.Count == 0) @@ -57,7 +58,7 @@ public async Task ResolveResourceIdAsync( if (!string.IsNullOrEmpty(resourceGroup)) { filteredResources = filteredResources.Where(r => - r.Data.Id?.ResourceGroupName?.Equals(resourceGroup, StringComparison.OrdinalIgnoreCase) == true); + r.Data.Id?.ResourceGroupName?.Equals(resourceGroup, StringComparisons.ResourceGroup) == true); } // Filter by resource type if provided diff --git a/tools/Azure.Mcp.Tools.Search/src/Services/SearchService.cs b/tools/Azure.Mcp.Tools.Search/src/Services/SearchService.cs index f2c1b9242b..03049c3a61 100644 --- a/tools/Azure.Mcp.Tools.Search/src/Services/SearchService.cs +++ b/tools/Azure.Mcp.Tools.Search/src/Services/SearchService.cs @@ -17,6 +17,7 @@ using Azure.Search.Documents.KnowledgeBases.Models; using Azure.Search.Documents.Models; using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Helpers; using Microsoft.Mcp.Core.Options; using Microsoft.Mcp.Core.Services.Azure.Authentication; using Microsoft.Mcp.Core.Services.Caching; @@ -215,7 +216,7 @@ public async Task> ListKnowledgeBases( var result = await searchClient.GetKnowledgeBaseAsync(knowledgeBaseName, cancellationToken: cancellationToken); if (result?.Value != null) { - if (result.Value.Name.Equals(knowledgeBaseName, StringComparison.OrdinalIgnoreCase)) + if (result.Value.Name.Equals(knowledgeBaseName, StringComparisons.ResourceName)) { bases.Add(new(result.Value.Name, result.Value.Description, [.. result.Value.KnowledgeSources.Select(ks => ks.Name)])); }