From 7fee3c004dc7f26161bac976554dd0f79ace01c7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 15:39:47 +0000 Subject: [PATCH 1/5] Initial plan From 08bbc1d0126d306031d20d52e641995ebcd37534 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 15:48:21 +0000 Subject: [PATCH 2/5] Add managed identity policy KQL generation for Azure Data Explorer databases Agent-Logs-Url: https://github.com/github/KustoSchemaTools/sessions/10ba8c30-dcb5-4cce-8f5c-7f54024a1299 Co-authored-by: alex-slynko <4385389+alex-slynko@users.noreply.github.com> --- .../Model/ManagedIdentityPolicyTests.cs | 154 ++++++++++++++++++ KustoSchemaTools/Changes/DatabaseChanges.cs | 4 + KustoSchemaTools/Model/Database.cs | 2 + .../Model/ManagedIdentityPolicy.cs | 20 +++ 4 files changed, 180 insertions(+) create mode 100644 KustoSchemaTools.Tests/Model/ManagedIdentityPolicyTests.cs create mode 100644 KustoSchemaTools/Model/ManagedIdentityPolicy.cs diff --git a/KustoSchemaTools.Tests/Model/ManagedIdentityPolicyTests.cs b/KustoSchemaTools.Tests/Model/ManagedIdentityPolicyTests.cs new file mode 100644 index 0000000..427917c --- /dev/null +++ b/KustoSchemaTools.Tests/Model/ManagedIdentityPolicyTests.cs @@ -0,0 +1,154 @@ +using KustoSchemaTools.Model; +using KustoSchemaTools.Changes; +using Microsoft.Extensions.Logging; +using Moq; + +namespace KustoSchemaTools.Tests.ManagedIdentity +{ + public class ManagedIdentityPolicyTests + { + [Fact] + public void CreateScript_SingleUsage_GeneratesCorrectKql() + { + // Arrange + var policy = new ManagedIdentityPolicy + { + ObjectId = "12345678-1234-1234-1234-123456789abc", + AllowedUsages = new List { "NativeIngestion" } + }; + + // Act + var script = policy.CreateScript("MyDatabase"); + + // Assert + Assert.Equal("ManagedIdentityPolicy", script.Kind); + Assert.Equal(80, script.Script.Order); + Assert.Contains(".alter-merge database MyDatabase policy managed_identity", script.Script.Text); + Assert.Contains("\"ObjectId\": \"12345678-1234-1234-1234-123456789abc\"", script.Script.Text); + Assert.Contains("\"AllowedUsages\": \"NativeIngestion\"", script.Script.Text); + } + + [Fact] + public void CreateScript_MultipleUsages_JoinsWithComma() + { + // Arrange + var policy = new ManagedIdentityPolicy + { + ObjectId = "12345678-1234-1234-1234-123456789abc", + AllowedUsages = new List { "AutomatedFlows", "ExternalTable", "NativeIngestion" } + }; + + // Act + var script = policy.CreateScript("MyDatabase"); + + // Assert + Assert.Contains("\"AllowedUsages\": \"AutomatedFlows, ExternalTable, NativeIngestion\"", script.Script.Text); + } + + [Fact] + public void CreateScript_DatabaseNameUsedInKql() + { + // Arrange + var policy = new ManagedIdentityPolicy + { + ObjectId = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + AllowedUsages = new List { "ExternalTable" } + }; + + // Act + var script = policy.CreateScript("TargetDatabase"); + + // Assert + Assert.StartsWith(".alter-merge database TargetDatabase policy managed_identity", script.Script.Text); + } + + [Fact] + public void CreateScript_WrapsJsonInBackticks() + { + // Arrange + var policy = new ManagedIdentityPolicy + { + ObjectId = "12345678-1234-1234-1234-123456789abc", + AllowedUsages = new List { "NativeIngestion" } + }; + + // Act + var script = policy.CreateScript("MyDatabase"); + + // Assert + Assert.Contains("```", script.Script.Text); + Assert.EndsWith("```", script.Script.Text); + } + + [Fact] + public void DatabaseChanges_WithManagedIdentityPolicies_GeneratesScript() + { + // Arrange + var loggerMock = new Mock(); + var oldState = new Database { Name = "TestDb" }; + var newState = new Database + { + Name = "TestDb", + ManagedIdentityPolicies = new List + { + new ManagedIdentityPolicy + { + ObjectId = "12345678-1234-1234-1234-123456789abc", + AllowedUsages = new List { "NativeIngestion" } + } + } + }; + + // Act + var changes = DatabaseChanges.GenerateChanges(oldState, newState, "TestDb", loggerMock.Object); + + // Assert + Assert.NotEmpty(changes); + var scripts = changes.SelectMany(c => c.Scripts).ToList(); + Assert.NotEmpty(scripts); + var managedIdentityScript = scripts.FirstOrDefault(s => s.Kind == "ManagedIdentityPolicy"); + Assert.NotNull(managedIdentityScript); + Assert.Contains(".alter-merge database TestDb policy managed_identity", managedIdentityScript.Script.Text); + Assert.Contains("12345678-1234-1234-1234-123456789abc", managedIdentityScript.Script.Text); + } + + [Fact] + public void DatabaseChanges_WithUnchangedManagedIdentityPolicies_GeneratesNoChanges() + { + // Arrange + var loggerMock = new Mock(); + var policy = new ManagedIdentityPolicy + { + ObjectId = "12345678-1234-1234-1234-123456789abc", + AllowedUsages = new List { "NativeIngestion" } + }; + var oldState = new Database + { + Name = "TestDb", + ManagedIdentityPolicies = new List { policy } + }; + var newState = new Database + { + Name = "TestDb", + ManagedIdentityPolicies = new List + { + new ManagedIdentityPolicy + { + ObjectId = "12345678-1234-1234-1234-123456789abc", + AllowedUsages = new List { "NativeIngestion" } + } + } + }; + + // Act + var changes = DatabaseChanges.GenerateChanges(oldState, newState, "TestDb", loggerMock.Object); + + // Assert - no database-level changes since policies are identical + var databaseScriptChanges = changes + .SelectMany(c => c.Scripts) + .Where(s => s.Kind == "ManagedIdentityPolicy") + .ToList(); + Assert.Empty(databaseScriptChanges); + } + } +} diff --git a/KustoSchemaTools/Changes/DatabaseChanges.cs b/KustoSchemaTools/Changes/DatabaseChanges.cs index 17f3a5a..84ed8af 100644 --- a/KustoSchemaTools/Changes/DatabaseChanges.cs +++ b/KustoSchemaTools/Changes/DatabaseChanges.cs @@ -22,6 +22,8 @@ public static List GenerateChanges(Database oldState, Database newState otherFromScripts.AddRange(oldState.Scripts.Select(itm => new DatabaseScriptContainer(itm, "DatabaseScript"))); if (oldState.DefaultRetentionAndCache != null) otherFromScripts.AddRange(oldState.DefaultRetentionAndCache.CreateScripts(name, "database")); + if (oldState.ManagedIdentityPolicies != null) + otherFromScripts.AddRange(oldState.ManagedIdentityPolicies.Select(p => p.CreateScript(name))); } var otherToScripts = new List(); @@ -29,6 +31,8 @@ public static List GenerateChanges(Database oldState, Database newState otherToScripts.AddRange(newState.Scripts.Select(itm => new DatabaseScriptContainer(itm, "DatabaseScript"))); if (newState.DefaultRetentionAndCache != null) otherToScripts.AddRange(newState.DefaultRetentionAndCache.CreateScripts(name, "database")); + if (newState.ManagedIdentityPolicies != null) + otherToScripts.AddRange(newState.ManagedIdentityPolicies.Select(p => p.CreateScript(name))); if (otherToScripts.Count > 0) { diff --git a/KustoSchemaTools/Model/Database.cs b/KustoSchemaTools/Model/Database.cs index e19340f..6d261cd 100644 --- a/KustoSchemaTools/Model/Database.cs +++ b/KustoSchemaTools/Model/Database.cs @@ -37,6 +37,8 @@ public class Database public Dictionary Followers { get; set; } = new Dictionary(); + public List ManagedIdentityPolicies { get; set; } = new List(); + public string EscapedName => Name.BracketIfIdentifier(); } } diff --git a/KustoSchemaTools/Model/ManagedIdentityPolicy.cs b/KustoSchemaTools/Model/ManagedIdentityPolicy.cs new file mode 100644 index 0000000..3babf14 --- /dev/null +++ b/KustoSchemaTools/Model/ManagedIdentityPolicy.cs @@ -0,0 +1,20 @@ +using KustoSchemaTools.Changes; +using Newtonsoft.Json; +using KustoSchemaTools.Helpers; +using KustoSchemaTools.Parser; + +namespace KustoSchemaTools.Model +{ + public class ManagedIdentityPolicy + { + public string ObjectId { get; set; } + public List AllowedUsages { get; set; } = new List(); + + public DatabaseScriptContainer CreateScript(string databaseName) + { + var policyObjects = new[] { new { ObjectId = ObjectId, AllowedUsages = string.Join(", ", AllowedUsages) } }; + var json = JsonConvert.SerializeObject(policyObjects, Serialization.JsonPascalCase); + return new DatabaseScriptContainer("ManagedIdentityPolicy", 80, $".alter-merge database {databaseName.BracketIfIdentifier()} policy managed_identity ```{json}```"); + } + } +} From c87b416d513344f0af657216a7faadc918bda178 Mon Sep 17 00:00:00 2001 From: Oleksandr Slynko Date: Thu, 9 Apr 2026 18:21:02 +0100 Subject: [PATCH 3/5] Add KustoManagedIdentityPolicyLoader and fix combined script generation - Add KustoManagedIdentityPolicyLoader to read managed identity policies from a live Kusto cluster during import mode - Change ManagedIdentityPolicy to use CreateCombinedScript static method that generates a single script for all policies, avoiding duplicate Kind keys in ScriptCompareChange.ToDictionary() - Sort policies by ObjectId and usages alphabetically for canonical diffs - Add demo managedIdentityPolicies to DemoDatabase database.yml - Update tests for new combined script API and add multi-policy test - Add YAML round-trip test for managed identity policies Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DemoDeployment/DemoDatabase/database.yml | 6 + .../Model/ManagedIdentityPolicyTests.cs | 117 +++++++++++++++--- .../Parser/YamlDatabaseHandlerTests.cs | 9 ++ KustoSchemaTools/Changes/DatabaseChanges.cs | 8 +- .../Model/ManagedIdentityPolicy.cs | 11 +- .../KustoManagedIdentityPolicyLoader.cs | 39 ++++++ 6 files changed, 164 insertions(+), 26 deletions(-) create mode 100644 KustoSchemaTools/Parser/KustoLoader/KustoManagedIdentityPolicyLoader.cs diff --git a/KustoSchemaTools.Tests/DemoData/DemoDeployment/DemoDatabase/database.yml b/KustoSchemaTools.Tests/DemoData/DemoDeployment/DemoDatabase/database.yml index c669ddc..ef49085 100644 --- a/KustoSchemaTools.Tests/DemoData/DemoDeployment/DemoDatabase/database.yml +++ b/KustoSchemaTools.Tests/DemoData/DemoDeployment/DemoDatabase/database.yml @@ -13,6 +13,12 @@ admins: - name: SPN-ADMIN id: aadapp=f678ce29-8f92-4d6e-b95d-f2ed8fa7713f;7396cfeb-2920-488f-b0bb-81a584d34a24 +managedIdentityPolicies: +- objectId: 12345678-1234-1234-1234-123456789abc + allowedUsages: + - NativeIngestion + - ExternalTable + tables: sourceTable: restrictedViewAccess: true diff --git a/KustoSchemaTools.Tests/Model/ManagedIdentityPolicyTests.cs b/KustoSchemaTools.Tests/Model/ManagedIdentityPolicyTests.cs index 427917c..610e66d 100644 --- a/KustoSchemaTools.Tests/Model/ManagedIdentityPolicyTests.cs +++ b/KustoSchemaTools.Tests/Model/ManagedIdentityPolicyTests.cs @@ -8,17 +8,20 @@ namespace KustoSchemaTools.Tests.ManagedIdentity public class ManagedIdentityPolicyTests { [Fact] - public void CreateScript_SingleUsage_GeneratesCorrectKql() + public void CreateCombinedScript_SinglePolicy_GeneratesCorrectKql() { // Arrange - var policy = new ManagedIdentityPolicy + var policies = new List { - ObjectId = "12345678-1234-1234-1234-123456789abc", - AllowedUsages = new List { "NativeIngestion" } + new ManagedIdentityPolicy + { + ObjectId = "12345678-1234-1234-1234-123456789abc", + AllowedUsages = new List { "NativeIngestion" } + } }; // Act - var script = policy.CreateScript("MyDatabase"); + var script = ManagedIdentityPolicy.CreateCombinedScript("MyDatabase", policies); // Assert Assert.Equal("ManagedIdentityPolicy", script.Kind); @@ -29,51 +32,88 @@ public void CreateScript_SingleUsage_GeneratesCorrectKql() } [Fact] - public void CreateScript_MultipleUsages_JoinsWithComma() + public void CreateCombinedScript_MultipleUsages_JoinsWithCommaAlphabetically() { // Arrange - var policy = new ManagedIdentityPolicy + var policies = new List { - ObjectId = "12345678-1234-1234-1234-123456789abc", - AllowedUsages = new List { "AutomatedFlows", "ExternalTable", "NativeIngestion" } + new ManagedIdentityPolicy + { + ObjectId = "12345678-1234-1234-1234-123456789abc", + AllowedUsages = new List { "NativeIngestion", "AutomatedFlows", "ExternalTable" } + } }; // Act - var script = policy.CreateScript("MyDatabase"); + var script = ManagedIdentityPolicy.CreateCombinedScript("MyDatabase", policies); // Assert Assert.Contains("\"AllowedUsages\": \"AutomatedFlows, ExternalTable, NativeIngestion\"", script.Script.Text); } [Fact] - public void CreateScript_DatabaseNameUsedInKql() + public void CreateCombinedScript_MultiplePolicies_SortsByObjectId() { // Arrange - var policy = new ManagedIdentityPolicy + var policies = new List { - ObjectId = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", - AllowedUsages = new List { "ExternalTable" } + new ManagedIdentityPolicy + { + ObjectId = "zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz", + AllowedUsages = new List { "ExternalTable" } + }, + new ManagedIdentityPolicy + { + ObjectId = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + AllowedUsages = new List { "NativeIngestion" } + } }; // Act - var script = policy.CreateScript("TargetDatabase"); + var script = ManagedIdentityPolicy.CreateCombinedScript("MyDatabase", policies); + + // Assert - both identities in a single script, sorted by ObjectId + Assert.Equal("ManagedIdentityPolicy", script.Kind); + var aIdx = script.Script.Text.IndexOf("aaaaaaaa"); + var zIdx = script.Script.Text.IndexOf("zzzzzzzz"); + Assert.True(aIdx < zIdx, "Policies should be sorted by ObjectId"); + } + + [Fact] + public void CreateCombinedScript_DatabaseNameUsedInKql() + { + // Arrange + var policies = new List + { + new ManagedIdentityPolicy + { + ObjectId = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + AllowedUsages = new List { "ExternalTable" } + } + }; + + // Act + var script = ManagedIdentityPolicy.CreateCombinedScript("TargetDatabase", policies); // Assert Assert.StartsWith(".alter-merge database TargetDatabase policy managed_identity", script.Script.Text); } [Fact] - public void CreateScript_WrapsJsonInBackticks() + public void CreateCombinedScript_WrapsJsonInBackticks() { // Arrange - var policy = new ManagedIdentityPolicy + var policies = new List { - ObjectId = "12345678-1234-1234-1234-123456789abc", - AllowedUsages = new List { "NativeIngestion" } + new ManagedIdentityPolicy + { + ObjectId = "12345678-1234-1234-1234-123456789abc", + AllowedUsages = new List { "NativeIngestion" } + } }; // Act - var script = policy.CreateScript("MyDatabase"); + var script = ManagedIdentityPolicy.CreateCombinedScript("MyDatabase", policies); // Assert Assert.Contains("```", script.Script.Text); @@ -112,6 +152,43 @@ public void DatabaseChanges_WithManagedIdentityPolicies_GeneratesScript() Assert.Contains("12345678-1234-1234-1234-123456789abc", managedIdentityScript.Script.Text); } + [Fact] + public void DatabaseChanges_WithMultipleManagedIdentityPolicies_GeneratesSingleScript() + { + // Arrange + var loggerMock = new Mock(); + var oldState = new Database { Name = "TestDb" }; + var newState = new Database + { + Name = "TestDb", + ManagedIdentityPolicies = new List + { + new ManagedIdentityPolicy + { + ObjectId = "aaaaaaaa-1111-2222-3333-444444444444", + AllowedUsages = new List { "NativeIngestion" } + }, + new ManagedIdentityPolicy + { + ObjectId = "bbbbbbbb-1111-2222-3333-444444444444", + AllowedUsages = new List { "ExternalTable" } + } + } + }; + + // Act + var changes = DatabaseChanges.GenerateChanges(oldState, newState, "TestDb", loggerMock.Object); + + // Assert - should generate exactly one ManagedIdentityPolicy script (combined) + var managedIdentityScripts = changes + .SelectMany(c => c.Scripts) + .Where(s => s.Kind == "ManagedIdentityPolicy") + .ToList(); + Assert.Single(managedIdentityScripts); + Assert.Contains("aaaaaaaa-1111-2222-3333-444444444444", managedIdentityScripts[0].Script.Text); + Assert.Contains("bbbbbbbb-1111-2222-3333-444444444444", managedIdentityScripts[0].Script.Text); + } + [Fact] public void DatabaseChanges_WithUnchangedManagedIdentityPolicies_GeneratesNoChanges() { diff --git a/KustoSchemaTools.Tests/Parser/YamlDatabaseHandlerTests.cs b/KustoSchemaTools.Tests/Parser/YamlDatabaseHandlerTests.cs index c5c4b71..111ff7c 100644 --- a/KustoSchemaTools.Tests/Parser/YamlDatabaseHandlerTests.cs +++ b/KustoSchemaTools.Tests/Parser/YamlDatabaseHandlerTests.cs @@ -41,6 +41,15 @@ public async Task GetDatabase() Assert.NotNull(tt.Policies); Assert.False(tt.Policies!.RestrictedViewAccess); Assert.Equal("120d", tt.Policies?.Retention); + + // Verify managed identity policies are loaded from database.yml + Assert.NotNull(db.ManagedIdentityPolicies); + Assert.Single(db.ManagedIdentityPolicies); + var miPolicy = db.ManagedIdentityPolicies[0]; + Assert.Equal("12345678-1234-1234-1234-123456789abc", miPolicy.ObjectId); + Assert.Equal(2, miPolicy.AllowedUsages.Count); + Assert.Contains("NativeIngestion", miPolicy.AllowedUsages); + Assert.Contains("ExternalTable", miPolicy.AllowedUsages); } [Fact] diff --git a/KustoSchemaTools/Changes/DatabaseChanges.cs b/KustoSchemaTools/Changes/DatabaseChanges.cs index 84ed8af..64dc709 100644 --- a/KustoSchemaTools/Changes/DatabaseChanges.cs +++ b/KustoSchemaTools/Changes/DatabaseChanges.cs @@ -22,8 +22,8 @@ public static List GenerateChanges(Database oldState, Database newState otherFromScripts.AddRange(oldState.Scripts.Select(itm => new DatabaseScriptContainer(itm, "DatabaseScript"))); if (oldState.DefaultRetentionAndCache != null) otherFromScripts.AddRange(oldState.DefaultRetentionAndCache.CreateScripts(name, "database")); - if (oldState.ManagedIdentityPolicies != null) - otherFromScripts.AddRange(oldState.ManagedIdentityPolicies.Select(p => p.CreateScript(name))); + if (oldState.ManagedIdentityPolicies != null && oldState.ManagedIdentityPolicies.Any()) + otherFromScripts.Add(ManagedIdentityPolicy.CreateCombinedScript(name, oldState.ManagedIdentityPolicies)); } var otherToScripts = new List(); @@ -31,8 +31,8 @@ public static List GenerateChanges(Database oldState, Database newState otherToScripts.AddRange(newState.Scripts.Select(itm => new DatabaseScriptContainer(itm, "DatabaseScript"))); if (newState.DefaultRetentionAndCache != null) otherToScripts.AddRange(newState.DefaultRetentionAndCache.CreateScripts(name, "database")); - if (newState.ManagedIdentityPolicies != null) - otherToScripts.AddRange(newState.ManagedIdentityPolicies.Select(p => p.CreateScript(name))); + if (newState.ManagedIdentityPolicies != null && newState.ManagedIdentityPolicies.Any()) + otherToScripts.Add(ManagedIdentityPolicy.CreateCombinedScript(name, newState.ManagedIdentityPolicies)); if (otherToScripts.Count > 0) { diff --git a/KustoSchemaTools/Model/ManagedIdentityPolicy.cs b/KustoSchemaTools/Model/ManagedIdentityPolicy.cs index 3babf14..cd5493e 100644 --- a/KustoSchemaTools/Model/ManagedIdentityPolicy.cs +++ b/KustoSchemaTools/Model/ManagedIdentityPolicy.cs @@ -10,9 +10,16 @@ public class ManagedIdentityPolicy public string ObjectId { get; set; } public List AllowedUsages { get; set; } = new List(); - public DatabaseScriptContainer CreateScript(string databaseName) + /// + /// Creates a single script that sets managed identity policy for all provided identities. + /// Uses one combined command to avoid duplicate Kind keys in the diff pipeline. + /// + public static DatabaseScriptContainer CreateCombinedScript(string databaseName, List policies) { - var policyObjects = new[] { new { ObjectId = ObjectId, AllowedUsages = string.Join(", ", AllowedUsages) } }; + var policyObjects = policies + .OrderBy(p => p.ObjectId) + .Select(p => new { p.ObjectId, AllowedUsages = string.Join(", ", p.AllowedUsages.OrderBy(u => u)) }) + .ToArray(); var json = JsonConvert.SerializeObject(policyObjects, Serialization.JsonPascalCase); return new DatabaseScriptContainer("ManagedIdentityPolicy", 80, $".alter-merge database {databaseName.BracketIfIdentifier()} policy managed_identity ```{json}```"); } diff --git a/KustoSchemaTools/Parser/KustoLoader/KustoManagedIdentityPolicyLoader.cs b/KustoSchemaTools/Parser/KustoLoader/KustoManagedIdentityPolicyLoader.cs new file mode 100644 index 0000000..58eb9d0 --- /dev/null +++ b/KustoSchemaTools/Parser/KustoLoader/KustoManagedIdentityPolicyLoader.cs @@ -0,0 +1,39 @@ +using Kusto.Data.Common; +using KustoSchemaTools.Model; +using KustoSchemaTools.Plugins; +using Newtonsoft.Json; + +namespace KustoSchemaTools.Parser.KustoLoader +{ + public class KustoManagedIdentityPolicyLoader : IKustoBulkEntitiesLoader + { + const string script = @" +.show database policy managed_identity +| project Policies = parse_json(Policies) +| mv-expand Policy = Policies +| project ObjectId = tostring(Policy.ObjectId), AllowedUsages = tostring(Policy.AllowedUsages)"; + + public async Task Load(Database database, string databaseName, KustoClient kusto) + { + var response = await kusto.Client.ExecuteQueryAsync(databaseName, script, new ClientRequestProperties()); + var rows = response.As(); + database.ManagedIdentityPolicies = rows + .Select(r => new ManagedIdentityPolicy + { + ObjectId = r.ObjectId, + AllowedUsages = r.AllowedUsages + .Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries) + .OrderBy(u => u) + .ToList() + }) + .OrderBy(p => p.ObjectId) + .ToList(); + } + + private class ManagedIdentityRow + { + public string ObjectId { get; set; } + public string AllowedUsages { get; set; } + } + } +} From d87ca96ee8fae81a2f5ec75e4bcf5a4070bea9a7 Mon Sep 17 00:00:00 2001 From: Oleksandr Slynko Date: Thu, 9 Apr 2026 19:26:04 +0100 Subject: [PATCH 4/5] Restructure to nested policies object in database YAML Introduce DatabasePolicies class so managed identity lives under policies.managedIdentity in YAML, making it extensible for future database-level policies. - Add DatabasePolicies model with ManagedIdentity property - Replace Database.ManagedIdentityPolicies with Database.Policies - Update DatabaseChanges, loader, tests, and demo YAML Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DemoDeployment/DemoDatabase/database.yml | 11 +++-- .../Model/ManagedIdentityPolicyTests.cs | 48 ++++++++++++------- .../Parser/YamlDatabaseHandlerTests.cs | 7 +-- KustoSchemaTools/Changes/DatabaseChanges.cs | 8 ++-- KustoSchemaTools/Model/Database.cs | 2 +- KustoSchemaTools/Model/DatabasePolicies.cs | 7 +++ .../KustoManagedIdentityPolicyLoader.cs | 4 +- 7 files changed, 55 insertions(+), 32 deletions(-) create mode 100644 KustoSchemaTools/Model/DatabasePolicies.cs diff --git a/KustoSchemaTools.Tests/DemoData/DemoDeployment/DemoDatabase/database.yml b/KustoSchemaTools.Tests/DemoData/DemoDeployment/DemoDatabase/database.yml index ef49085..cfa2ba2 100644 --- a/KustoSchemaTools.Tests/DemoData/DemoDeployment/DemoDatabase/database.yml +++ b/KustoSchemaTools.Tests/DemoData/DemoDeployment/DemoDatabase/database.yml @@ -13,11 +13,12 @@ admins: - name: SPN-ADMIN id: aadapp=f678ce29-8f92-4d6e-b95d-f2ed8fa7713f;7396cfeb-2920-488f-b0bb-81a584d34a24 -managedIdentityPolicies: -- objectId: 12345678-1234-1234-1234-123456789abc - allowedUsages: - - NativeIngestion - - ExternalTable +policies: + managedIdentity: + - objectId: 12345678-1234-1234-1234-123456789abc + allowedUsages: + - NativeIngestion + - ExternalTable tables: sourceTable: diff --git a/KustoSchemaTools.Tests/Model/ManagedIdentityPolicyTests.cs b/KustoSchemaTools.Tests/Model/ManagedIdentityPolicyTests.cs index 610e66d..66e6ff0 100644 --- a/KustoSchemaTools.Tests/Model/ManagedIdentityPolicyTests.cs +++ b/KustoSchemaTools.Tests/Model/ManagedIdentityPolicyTests.cs @@ -129,12 +129,15 @@ public void DatabaseChanges_WithManagedIdentityPolicies_GeneratesScript() var newState = new Database { Name = "TestDb", - ManagedIdentityPolicies = new List + Policies = new DatabasePolicies { - new ManagedIdentityPolicy + ManagedIdentity = new List { - ObjectId = "12345678-1234-1234-1234-123456789abc", - AllowedUsages = new List { "NativeIngestion" } + new ManagedIdentityPolicy + { + ObjectId = "12345678-1234-1234-1234-123456789abc", + AllowedUsages = new List { "NativeIngestion" } + } } } }; @@ -161,17 +164,20 @@ public void DatabaseChanges_WithMultipleManagedIdentityPolicies_GeneratesSingleS var newState = new Database { Name = "TestDb", - ManagedIdentityPolicies = new List + Policies = new DatabasePolicies { - new ManagedIdentityPolicy + ManagedIdentity = new List { - ObjectId = "aaaaaaaa-1111-2222-3333-444444444444", - AllowedUsages = new List { "NativeIngestion" } - }, - new ManagedIdentityPolicy - { - ObjectId = "bbbbbbbb-1111-2222-3333-444444444444", - AllowedUsages = new List { "ExternalTable" } + new ManagedIdentityPolicy + { + ObjectId = "aaaaaaaa-1111-2222-3333-444444444444", + AllowedUsages = new List { "NativeIngestion" } + }, + new ManagedIdentityPolicy + { + ObjectId = "bbbbbbbb-1111-2222-3333-444444444444", + AllowedUsages = new List { "ExternalTable" } + } } } }; @@ -202,17 +208,23 @@ public void DatabaseChanges_WithUnchangedManagedIdentityPolicies_GeneratesNoChan var oldState = new Database { Name = "TestDb", - ManagedIdentityPolicies = new List { policy } + Policies = new DatabasePolicies + { + ManagedIdentity = new List { policy } + } }; var newState = new Database { Name = "TestDb", - ManagedIdentityPolicies = new List + Policies = new DatabasePolicies { - new ManagedIdentityPolicy + ManagedIdentity = new List { - ObjectId = "12345678-1234-1234-1234-123456789abc", - AllowedUsages = new List { "NativeIngestion" } + new ManagedIdentityPolicy + { + ObjectId = "12345678-1234-1234-1234-123456789abc", + AllowedUsages = new List { "NativeIngestion" } + } } } }; diff --git a/KustoSchemaTools.Tests/Parser/YamlDatabaseHandlerTests.cs b/KustoSchemaTools.Tests/Parser/YamlDatabaseHandlerTests.cs index 111ff7c..ce00444 100644 --- a/KustoSchemaTools.Tests/Parser/YamlDatabaseHandlerTests.cs +++ b/KustoSchemaTools.Tests/Parser/YamlDatabaseHandlerTests.cs @@ -43,9 +43,10 @@ public async Task GetDatabase() Assert.Equal("120d", tt.Policies?.Retention); // Verify managed identity policies are loaded from database.yml - Assert.NotNull(db.ManagedIdentityPolicies); - Assert.Single(db.ManagedIdentityPolicies); - var miPolicy = db.ManagedIdentityPolicies[0]; + Assert.NotNull(db.Policies); + Assert.NotNull(db.Policies.ManagedIdentity); + Assert.Single(db.Policies.ManagedIdentity); + var miPolicy = db.Policies.ManagedIdentity[0]; Assert.Equal("12345678-1234-1234-1234-123456789abc", miPolicy.ObjectId); Assert.Equal(2, miPolicy.AllowedUsages.Count); Assert.Contains("NativeIngestion", miPolicy.AllowedUsages); diff --git a/KustoSchemaTools/Changes/DatabaseChanges.cs b/KustoSchemaTools/Changes/DatabaseChanges.cs index 64dc709..0770927 100644 --- a/KustoSchemaTools/Changes/DatabaseChanges.cs +++ b/KustoSchemaTools/Changes/DatabaseChanges.cs @@ -22,8 +22,8 @@ public static List GenerateChanges(Database oldState, Database newState otherFromScripts.AddRange(oldState.Scripts.Select(itm => new DatabaseScriptContainer(itm, "DatabaseScript"))); if (oldState.DefaultRetentionAndCache != null) otherFromScripts.AddRange(oldState.DefaultRetentionAndCache.CreateScripts(name, "database")); - if (oldState.ManagedIdentityPolicies != null && oldState.ManagedIdentityPolicies.Any()) - otherFromScripts.Add(ManagedIdentityPolicy.CreateCombinedScript(name, oldState.ManagedIdentityPolicies)); + if (oldState.Policies?.ManagedIdentity != null && oldState.Policies.ManagedIdentity.Any()) + otherFromScripts.Add(ManagedIdentityPolicy.CreateCombinedScript(name, oldState.Policies.ManagedIdentity)); } var otherToScripts = new List(); @@ -31,8 +31,8 @@ public static List GenerateChanges(Database oldState, Database newState otherToScripts.AddRange(newState.Scripts.Select(itm => new DatabaseScriptContainer(itm, "DatabaseScript"))); if (newState.DefaultRetentionAndCache != null) otherToScripts.AddRange(newState.DefaultRetentionAndCache.CreateScripts(name, "database")); - if (newState.ManagedIdentityPolicies != null && newState.ManagedIdentityPolicies.Any()) - otherToScripts.Add(ManagedIdentityPolicy.CreateCombinedScript(name, newState.ManagedIdentityPolicies)); + if (newState.Policies?.ManagedIdentity != null && newState.Policies.ManagedIdentity.Any()) + otherToScripts.Add(ManagedIdentityPolicy.CreateCombinedScript(name, newState.Policies.ManagedIdentity)); if (otherToScripts.Count > 0) { diff --git a/KustoSchemaTools/Model/Database.cs b/KustoSchemaTools/Model/Database.cs index 6d261cd..84eacad 100644 --- a/KustoSchemaTools/Model/Database.cs +++ b/KustoSchemaTools/Model/Database.cs @@ -37,7 +37,7 @@ public class Database public Dictionary Followers { get; set; } = new Dictionary(); - public List ManagedIdentityPolicies { get; set; } = new List(); + public DatabasePolicies Policies { get; set; } = new DatabasePolicies(); public string EscapedName => Name.BracketIfIdentifier(); } diff --git a/KustoSchemaTools/Model/DatabasePolicies.cs b/KustoSchemaTools/Model/DatabasePolicies.cs new file mode 100644 index 0000000..d6144d7 --- /dev/null +++ b/KustoSchemaTools/Model/DatabasePolicies.cs @@ -0,0 +1,7 @@ +namespace KustoSchemaTools.Model +{ + public class DatabasePolicies + { + public List ManagedIdentity { get; set; } = new List(); + } +} diff --git a/KustoSchemaTools/Parser/KustoLoader/KustoManagedIdentityPolicyLoader.cs b/KustoSchemaTools/Parser/KustoLoader/KustoManagedIdentityPolicyLoader.cs index 58eb9d0..7170e88 100644 --- a/KustoSchemaTools/Parser/KustoLoader/KustoManagedIdentityPolicyLoader.cs +++ b/KustoSchemaTools/Parser/KustoLoader/KustoManagedIdentityPolicyLoader.cs @@ -17,7 +17,9 @@ public async Task Load(Database database, string databaseName, KustoClient kusto { var response = await kusto.Client.ExecuteQueryAsync(databaseName, script, new ClientRequestProperties()); var rows = response.As(); - database.ManagedIdentityPolicies = rows + if (database.Policies == null) + database.Policies = new DatabasePolicies(); + database.Policies.ManagedIdentity = rows .Select(r => new ManagedIdentityPolicy { ObjectId = r.ObjectId, From d49a0f9fb2cbf9d8e6966ad14cdf3da182a548ab Mon Sep 17 00:00:00 2001 From: Oleksandr Slynko Date: Fri, 10 Apr 2026 11:36:57 +0100 Subject: [PATCH 5/5] Fix Policy column name in managed identity loader MS docs say Policies (plural) but the actual Kusto cluster returns Policy (singular). One-character fix. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Parser/KustoLoader/KustoManagedIdentityPolicyLoader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/KustoSchemaTools/Parser/KustoLoader/KustoManagedIdentityPolicyLoader.cs b/KustoSchemaTools/Parser/KustoLoader/KustoManagedIdentityPolicyLoader.cs index 7170e88..bfbb019 100644 --- a/KustoSchemaTools/Parser/KustoLoader/KustoManagedIdentityPolicyLoader.cs +++ b/KustoSchemaTools/Parser/KustoLoader/KustoManagedIdentityPolicyLoader.cs @@ -9,7 +9,7 @@ public class KustoManagedIdentityPolicyLoader : IKustoBulkEntitiesLoader { const string script = @" .show database policy managed_identity -| project Policies = parse_json(Policies) +| project Policies = parse_json(Policy) | mv-expand Policy = Policies | project ObjectId = tostring(Policy.ObjectId), AllowedUsages = tostring(Policy.AllowedUsages)";