Skip to content

Commit c87b416

Browse files
alex-slynkoCopilot
andcommitted
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>
1 parent 08bbc1d commit c87b416

6 files changed

Lines changed: 164 additions & 26 deletions

File tree

KustoSchemaTools.Tests/DemoData/DemoDeployment/DemoDatabase/database.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ admins:
1313
- name: SPN-ADMIN
1414
id: aadapp=f678ce29-8f92-4d6e-b95d-f2ed8fa7713f;7396cfeb-2920-488f-b0bb-81a584d34a24
1515

16+
managedIdentityPolicies:
17+
- objectId: 12345678-1234-1234-1234-123456789abc
18+
allowedUsages:
19+
- NativeIngestion
20+
- ExternalTable
21+
1622
tables:
1723
sourceTable:
1824
restrictedViewAccess: true

KustoSchemaTools.Tests/Model/ManagedIdentityPolicyTests.cs

Lines changed: 97 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,20 @@ namespace KustoSchemaTools.Tests.ManagedIdentity
88
public class ManagedIdentityPolicyTests
99
{
1010
[Fact]
11-
public void CreateScript_SingleUsage_GeneratesCorrectKql()
11+
public void CreateCombinedScript_SinglePolicy_GeneratesCorrectKql()
1212
{
1313
// Arrange
14-
var policy = new ManagedIdentityPolicy
14+
var policies = new List<ManagedIdentityPolicy>
1515
{
16-
ObjectId = "12345678-1234-1234-1234-123456789abc",
17-
AllowedUsages = new List<string> { "NativeIngestion" }
16+
new ManagedIdentityPolicy
17+
{
18+
ObjectId = "12345678-1234-1234-1234-123456789abc",
19+
AllowedUsages = new List<string> { "NativeIngestion" }
20+
}
1821
};
1922

2023
// Act
21-
var script = policy.CreateScript("MyDatabase");
24+
var script = ManagedIdentityPolicy.CreateCombinedScript("MyDatabase", policies);
2225

2326
// Assert
2427
Assert.Equal("ManagedIdentityPolicy", script.Kind);
@@ -29,51 +32,88 @@ public void CreateScript_SingleUsage_GeneratesCorrectKql()
2932
}
3033

3134
[Fact]
32-
public void CreateScript_MultipleUsages_JoinsWithComma()
35+
public void CreateCombinedScript_MultipleUsages_JoinsWithCommaAlphabetically()
3336
{
3437
// Arrange
35-
var policy = new ManagedIdentityPolicy
38+
var policies = new List<ManagedIdentityPolicy>
3639
{
37-
ObjectId = "12345678-1234-1234-1234-123456789abc",
38-
AllowedUsages = new List<string> { "AutomatedFlows", "ExternalTable", "NativeIngestion" }
40+
new ManagedIdentityPolicy
41+
{
42+
ObjectId = "12345678-1234-1234-1234-123456789abc",
43+
AllowedUsages = new List<string> { "NativeIngestion", "AutomatedFlows", "ExternalTable" }
44+
}
3945
};
4046

4147
// Act
42-
var script = policy.CreateScript("MyDatabase");
48+
var script = ManagedIdentityPolicy.CreateCombinedScript("MyDatabase", policies);
4349

4450
// Assert
4551
Assert.Contains("\"AllowedUsages\": \"AutomatedFlows, ExternalTable, NativeIngestion\"", script.Script.Text);
4652
}
4753

4854
[Fact]
49-
public void CreateScript_DatabaseNameUsedInKql()
55+
public void CreateCombinedScript_MultiplePolicies_SortsByObjectId()
5056
{
5157
// Arrange
52-
var policy = new ManagedIdentityPolicy
58+
var policies = new List<ManagedIdentityPolicy>
5359
{
54-
ObjectId = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
55-
AllowedUsages = new List<string> { "ExternalTable" }
60+
new ManagedIdentityPolicy
61+
{
62+
ObjectId = "zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz",
63+
AllowedUsages = new List<string> { "ExternalTable" }
64+
},
65+
new ManagedIdentityPolicy
66+
{
67+
ObjectId = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
68+
AllowedUsages = new List<string> { "NativeIngestion" }
69+
}
5670
};
5771

5872
// Act
59-
var script = policy.CreateScript("TargetDatabase");
73+
var script = ManagedIdentityPolicy.CreateCombinedScript("MyDatabase", policies);
74+
75+
// Assert - both identities in a single script, sorted by ObjectId
76+
Assert.Equal("ManagedIdentityPolicy", script.Kind);
77+
var aIdx = script.Script.Text.IndexOf("aaaaaaaa");
78+
var zIdx = script.Script.Text.IndexOf("zzzzzzzz");
79+
Assert.True(aIdx < zIdx, "Policies should be sorted by ObjectId");
80+
}
81+
82+
[Fact]
83+
public void CreateCombinedScript_DatabaseNameUsedInKql()
84+
{
85+
// Arrange
86+
var policies = new List<ManagedIdentityPolicy>
87+
{
88+
new ManagedIdentityPolicy
89+
{
90+
ObjectId = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
91+
AllowedUsages = new List<string> { "ExternalTable" }
92+
}
93+
};
94+
95+
// Act
96+
var script = ManagedIdentityPolicy.CreateCombinedScript("TargetDatabase", policies);
6097

6198
// Assert
6299
Assert.StartsWith(".alter-merge database TargetDatabase policy managed_identity", script.Script.Text);
63100
}
64101

65102
[Fact]
66-
public void CreateScript_WrapsJsonInBackticks()
103+
public void CreateCombinedScript_WrapsJsonInBackticks()
67104
{
68105
// Arrange
69-
var policy = new ManagedIdentityPolicy
106+
var policies = new List<ManagedIdentityPolicy>
70107
{
71-
ObjectId = "12345678-1234-1234-1234-123456789abc",
72-
AllowedUsages = new List<string> { "NativeIngestion" }
108+
new ManagedIdentityPolicy
109+
{
110+
ObjectId = "12345678-1234-1234-1234-123456789abc",
111+
AllowedUsages = new List<string> { "NativeIngestion" }
112+
}
73113
};
74114

75115
// Act
76-
var script = policy.CreateScript("MyDatabase");
116+
var script = ManagedIdentityPolicy.CreateCombinedScript("MyDatabase", policies);
77117

78118
// Assert
79119
Assert.Contains("```", script.Script.Text);
@@ -112,6 +152,43 @@ public void DatabaseChanges_WithManagedIdentityPolicies_GeneratesScript()
112152
Assert.Contains("12345678-1234-1234-1234-123456789abc", managedIdentityScript.Script.Text);
113153
}
114154

155+
[Fact]
156+
public void DatabaseChanges_WithMultipleManagedIdentityPolicies_GeneratesSingleScript()
157+
{
158+
// Arrange
159+
var loggerMock = new Mock<ILogger>();
160+
var oldState = new Database { Name = "TestDb" };
161+
var newState = new Database
162+
{
163+
Name = "TestDb",
164+
ManagedIdentityPolicies = new List<ManagedIdentityPolicy>
165+
{
166+
new ManagedIdentityPolicy
167+
{
168+
ObjectId = "aaaaaaaa-1111-2222-3333-444444444444",
169+
AllowedUsages = new List<string> { "NativeIngestion" }
170+
},
171+
new ManagedIdentityPolicy
172+
{
173+
ObjectId = "bbbbbbbb-1111-2222-3333-444444444444",
174+
AllowedUsages = new List<string> { "ExternalTable" }
175+
}
176+
}
177+
};
178+
179+
// Act
180+
var changes = DatabaseChanges.GenerateChanges(oldState, newState, "TestDb", loggerMock.Object);
181+
182+
// Assert - should generate exactly one ManagedIdentityPolicy script (combined)
183+
var managedIdentityScripts = changes
184+
.SelectMany(c => c.Scripts)
185+
.Where(s => s.Kind == "ManagedIdentityPolicy")
186+
.ToList();
187+
Assert.Single(managedIdentityScripts);
188+
Assert.Contains("aaaaaaaa-1111-2222-3333-444444444444", managedIdentityScripts[0].Script.Text);
189+
Assert.Contains("bbbbbbbb-1111-2222-3333-444444444444", managedIdentityScripts[0].Script.Text);
190+
}
191+
115192
[Fact]
116193
public void DatabaseChanges_WithUnchangedManagedIdentityPolicies_GeneratesNoChanges()
117194
{

KustoSchemaTools.Tests/Parser/YamlDatabaseHandlerTests.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,15 @@ public async Task GetDatabase()
4141
Assert.NotNull(tt.Policies);
4242
Assert.False(tt.Policies!.RestrictedViewAccess);
4343
Assert.Equal("120d", tt.Policies?.Retention);
44+
45+
// Verify managed identity policies are loaded from database.yml
46+
Assert.NotNull(db.ManagedIdentityPolicies);
47+
Assert.Single(db.ManagedIdentityPolicies);
48+
var miPolicy = db.ManagedIdentityPolicies[0];
49+
Assert.Equal("12345678-1234-1234-1234-123456789abc", miPolicy.ObjectId);
50+
Assert.Equal(2, miPolicy.AllowedUsages.Count);
51+
Assert.Contains("NativeIngestion", miPolicy.AllowedUsages);
52+
Assert.Contains("ExternalTable", miPolicy.AllowedUsages);
4453
}
4554

4655
[Fact]

KustoSchemaTools/Changes/DatabaseChanges.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,17 @@ public static List<IChange> GenerateChanges(Database oldState, Database newState
2222
otherFromScripts.AddRange(oldState.Scripts.Select(itm => new DatabaseScriptContainer(itm, "DatabaseScript")));
2323
if (oldState.DefaultRetentionAndCache != null)
2424
otherFromScripts.AddRange(oldState.DefaultRetentionAndCache.CreateScripts(name, "database"));
25-
if (oldState.ManagedIdentityPolicies != null)
26-
otherFromScripts.AddRange(oldState.ManagedIdentityPolicies.Select(p => p.CreateScript(name)));
25+
if (oldState.ManagedIdentityPolicies != null && oldState.ManagedIdentityPolicies.Any())
26+
otherFromScripts.Add(ManagedIdentityPolicy.CreateCombinedScript(name, oldState.ManagedIdentityPolicies));
2727
}
2828

2929
var otherToScripts = new List<DatabaseScriptContainer>();
3030
if (newState.Scripts != null)
3131
otherToScripts.AddRange(newState.Scripts.Select(itm => new DatabaseScriptContainer(itm, "DatabaseScript")));
3232
if (newState.DefaultRetentionAndCache != null)
3333
otherToScripts.AddRange(newState.DefaultRetentionAndCache.CreateScripts(name, "database"));
34-
if (newState.ManagedIdentityPolicies != null)
35-
otherToScripts.AddRange(newState.ManagedIdentityPolicies.Select(p => p.CreateScript(name)));
34+
if (newState.ManagedIdentityPolicies != null && newState.ManagedIdentityPolicies.Any())
35+
otherToScripts.Add(ManagedIdentityPolicy.CreateCombinedScript(name, newState.ManagedIdentityPolicies));
3636

3737
if (otherToScripts.Count > 0)
3838
{

KustoSchemaTools/Model/ManagedIdentityPolicy.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,16 @@ public class ManagedIdentityPolicy
1010
public string ObjectId { get; set; }
1111
public List<string> AllowedUsages { get; set; } = new List<string>();
1212

13-
public DatabaseScriptContainer CreateScript(string databaseName)
13+
/// <summary>
14+
/// Creates a single script that sets managed identity policy for all provided identities.
15+
/// Uses one combined command to avoid duplicate Kind keys in the diff pipeline.
16+
/// </summary>
17+
public static DatabaseScriptContainer CreateCombinedScript(string databaseName, List<ManagedIdentityPolicy> policies)
1418
{
15-
var policyObjects = new[] { new { ObjectId = ObjectId, AllowedUsages = string.Join(", ", AllowedUsages) } };
19+
var policyObjects = policies
20+
.OrderBy(p => p.ObjectId)
21+
.Select(p => new { p.ObjectId, AllowedUsages = string.Join(", ", p.AllowedUsages.OrderBy(u => u)) })
22+
.ToArray();
1623
var json = JsonConvert.SerializeObject(policyObjects, Serialization.JsonPascalCase);
1724
return new DatabaseScriptContainer("ManagedIdentityPolicy", 80, $".alter-merge database {databaseName.BracketIfIdentifier()} policy managed_identity ```{json}```");
1825
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using Kusto.Data.Common;
2+
using KustoSchemaTools.Model;
3+
using KustoSchemaTools.Plugins;
4+
using Newtonsoft.Json;
5+
6+
namespace KustoSchemaTools.Parser.KustoLoader
7+
{
8+
public class KustoManagedIdentityPolicyLoader : IKustoBulkEntitiesLoader
9+
{
10+
const string script = @"
11+
.show database policy managed_identity
12+
| project Policies = parse_json(Policies)
13+
| mv-expand Policy = Policies
14+
| project ObjectId = tostring(Policy.ObjectId), AllowedUsages = tostring(Policy.AllowedUsages)";
15+
16+
public async Task Load(Database database, string databaseName, KustoClient kusto)
17+
{
18+
var response = await kusto.Client.ExecuteQueryAsync(databaseName, script, new ClientRequestProperties());
19+
var rows = response.As<ManagedIdentityRow>();
20+
database.ManagedIdentityPolicies = rows
21+
.Select(r => new ManagedIdentityPolicy
22+
{
23+
ObjectId = r.ObjectId,
24+
AllowedUsages = r.AllowedUsages
25+
.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
26+
.OrderBy(u => u)
27+
.ToList()
28+
})
29+
.OrderBy(p => p.ObjectId)
30+
.ToList();
31+
}
32+
33+
private class ManagedIdentityRow
34+
{
35+
public string ObjectId { get; set; }
36+
public string AllowedUsages { get; set; }
37+
}
38+
}
39+
}

0 commit comments

Comments
 (0)