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/2] 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/2] 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}```"); + } + } +}