Skip to content

Commit 77a6bed

Browse files
authored
Merge branch 'main' into fix/delta-table-kind-detection
2 parents aa05716 + 88c62f3 commit 77a6bed

File tree

8 files changed

+341
-0
lines changed

8 files changed

+341
-0
lines changed

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

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

16+
policies:
17+
managedIdentity:
18+
- objectId: 12345678-1234-1234-1234-123456789abc
19+
allowedUsages:
20+
- NativeIngestion
21+
- ExternalTable
22+
1623
tables:
1724
sourceTable:
1825
restrictedViewAccess: true
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
using KustoSchemaTools.Model;
2+
using KustoSchemaTools.Changes;
3+
using Microsoft.Extensions.Logging;
4+
using Moq;
5+
6+
namespace KustoSchemaTools.Tests.ManagedIdentity
7+
{
8+
public class ManagedIdentityPolicyTests
9+
{
10+
[Fact]
11+
public void CreateCombinedScript_SinglePolicy_GeneratesCorrectKql()
12+
{
13+
// Arrange
14+
var policies = new List<ManagedIdentityPolicy>
15+
{
16+
new ManagedIdentityPolicy
17+
{
18+
ObjectId = "12345678-1234-1234-1234-123456789abc",
19+
AllowedUsages = new List<string> { "NativeIngestion" }
20+
}
21+
};
22+
23+
// Act
24+
var script = ManagedIdentityPolicy.CreateCombinedScript("MyDatabase", policies);
25+
26+
// Assert
27+
Assert.Equal("ManagedIdentityPolicy", script.Kind);
28+
Assert.Equal(80, script.Script.Order);
29+
Assert.Contains(".alter-merge database MyDatabase policy managed_identity", script.Script.Text);
30+
Assert.Contains("\"ObjectId\": \"12345678-1234-1234-1234-123456789abc\"", script.Script.Text);
31+
Assert.Contains("\"AllowedUsages\": \"NativeIngestion\"", script.Script.Text);
32+
}
33+
34+
[Fact]
35+
public void CreateCombinedScript_MultipleUsages_JoinsWithCommaAlphabetically()
36+
{
37+
// Arrange
38+
var policies = new List<ManagedIdentityPolicy>
39+
{
40+
new ManagedIdentityPolicy
41+
{
42+
ObjectId = "12345678-1234-1234-1234-123456789abc",
43+
AllowedUsages = new List<string> { "NativeIngestion", "AutomatedFlows", "ExternalTable" }
44+
}
45+
};
46+
47+
// Act
48+
var script = ManagedIdentityPolicy.CreateCombinedScript("MyDatabase", policies);
49+
50+
// Assert
51+
Assert.Contains("\"AllowedUsages\": \"AutomatedFlows, ExternalTable, NativeIngestion\"", script.Script.Text);
52+
}
53+
54+
[Fact]
55+
public void CreateCombinedScript_MultiplePolicies_SortsByObjectId()
56+
{
57+
// Arrange
58+
var policies = new List<ManagedIdentityPolicy>
59+
{
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+
}
70+
};
71+
72+
// Act
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);
97+
98+
// Assert
99+
Assert.StartsWith(".alter-merge database TargetDatabase policy managed_identity", script.Script.Text);
100+
}
101+
102+
[Fact]
103+
public void CreateCombinedScript_WrapsJsonInBackticks()
104+
{
105+
// Arrange
106+
var policies = new List<ManagedIdentityPolicy>
107+
{
108+
new ManagedIdentityPolicy
109+
{
110+
ObjectId = "12345678-1234-1234-1234-123456789abc",
111+
AllowedUsages = new List<string> { "NativeIngestion" }
112+
}
113+
};
114+
115+
// Act
116+
var script = ManagedIdentityPolicy.CreateCombinedScript("MyDatabase", policies);
117+
118+
// Assert
119+
Assert.Contains("```", script.Script.Text);
120+
Assert.EndsWith("```", script.Script.Text);
121+
}
122+
123+
[Fact]
124+
public void DatabaseChanges_WithManagedIdentityPolicies_GeneratesScript()
125+
{
126+
// Arrange
127+
var loggerMock = new Mock<ILogger>();
128+
var oldState = new Database { Name = "TestDb" };
129+
var newState = new Database
130+
{
131+
Name = "TestDb",
132+
Policies = new DatabasePolicies
133+
{
134+
ManagedIdentity = new List<ManagedIdentityPolicy>
135+
{
136+
new ManagedIdentityPolicy
137+
{
138+
ObjectId = "12345678-1234-1234-1234-123456789abc",
139+
AllowedUsages = new List<string> { "NativeIngestion" }
140+
}
141+
}
142+
}
143+
};
144+
145+
// Act
146+
var changes = DatabaseChanges.GenerateChanges(oldState, newState, "TestDb", loggerMock.Object);
147+
148+
// Assert
149+
Assert.NotEmpty(changes);
150+
var scripts = changes.SelectMany(c => c.Scripts).ToList();
151+
Assert.NotEmpty(scripts);
152+
var managedIdentityScript = scripts.FirstOrDefault(s => s.Kind == "ManagedIdentityPolicy");
153+
Assert.NotNull(managedIdentityScript);
154+
Assert.Contains(".alter-merge database TestDb policy managed_identity", managedIdentityScript.Script.Text);
155+
Assert.Contains("12345678-1234-1234-1234-123456789abc", managedIdentityScript.Script.Text);
156+
}
157+
158+
[Fact]
159+
public void DatabaseChanges_WithMultipleManagedIdentityPolicies_GeneratesSingleScript()
160+
{
161+
// Arrange
162+
var loggerMock = new Mock<ILogger>();
163+
var oldState = new Database { Name = "TestDb" };
164+
var newState = new Database
165+
{
166+
Name = "TestDb",
167+
Policies = new DatabasePolicies
168+
{
169+
ManagedIdentity = new List<ManagedIdentityPolicy>
170+
{
171+
new ManagedIdentityPolicy
172+
{
173+
ObjectId = "aaaaaaaa-1111-2222-3333-444444444444",
174+
AllowedUsages = new List<string> { "NativeIngestion" }
175+
},
176+
new ManagedIdentityPolicy
177+
{
178+
ObjectId = "bbbbbbbb-1111-2222-3333-444444444444",
179+
AllowedUsages = new List<string> { "ExternalTable" }
180+
}
181+
}
182+
}
183+
};
184+
185+
// Act
186+
var changes = DatabaseChanges.GenerateChanges(oldState, newState, "TestDb", loggerMock.Object);
187+
188+
// Assert - should generate exactly one ManagedIdentityPolicy script (combined)
189+
var managedIdentityScripts = changes
190+
.SelectMany(c => c.Scripts)
191+
.Where(s => s.Kind == "ManagedIdentityPolicy")
192+
.ToList();
193+
Assert.Single(managedIdentityScripts);
194+
Assert.Contains("aaaaaaaa-1111-2222-3333-444444444444", managedIdentityScripts[0].Script.Text);
195+
Assert.Contains("bbbbbbbb-1111-2222-3333-444444444444", managedIdentityScripts[0].Script.Text);
196+
}
197+
198+
[Fact]
199+
public void DatabaseChanges_WithUnchangedManagedIdentityPolicies_GeneratesNoChanges()
200+
{
201+
// Arrange
202+
var loggerMock = new Mock<ILogger>();
203+
var policy = new ManagedIdentityPolicy
204+
{
205+
ObjectId = "12345678-1234-1234-1234-123456789abc",
206+
AllowedUsages = new List<string> { "NativeIngestion" }
207+
};
208+
var oldState = new Database
209+
{
210+
Name = "TestDb",
211+
Policies = new DatabasePolicies
212+
{
213+
ManagedIdentity = new List<ManagedIdentityPolicy> { policy }
214+
}
215+
};
216+
var newState = new Database
217+
{
218+
Name = "TestDb",
219+
Policies = new DatabasePolicies
220+
{
221+
ManagedIdentity = new List<ManagedIdentityPolicy>
222+
{
223+
new ManagedIdentityPolicy
224+
{
225+
ObjectId = "12345678-1234-1234-1234-123456789abc",
226+
AllowedUsages = new List<string> { "NativeIngestion" }
227+
}
228+
}
229+
}
230+
};
231+
232+
// Act
233+
var changes = DatabaseChanges.GenerateChanges(oldState, newState, "TestDb", loggerMock.Object);
234+
235+
// Assert - no database-level changes since policies are identical
236+
var databaseScriptChanges = changes
237+
.SelectMany(c => c.Scripts)
238+
.Where(s => s.Kind == "ManagedIdentityPolicy")
239+
.ToList();
240+
Assert.Empty(databaseScriptChanges);
241+
}
242+
}
243+
}

KustoSchemaTools.Tests/Parser/YamlDatabaseHandlerTests.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,16 @@ 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.Policies);
47+
Assert.NotNull(db.Policies.ManagedIdentity);
48+
Assert.Single(db.Policies.ManagedIdentity);
49+
var miPolicy = db.Policies.ManagedIdentity[0];
50+
Assert.Equal("12345678-1234-1234-1234-123456789abc", miPolicy.ObjectId);
51+
Assert.Equal(2, miPolicy.AllowedUsages.Count);
52+
Assert.Contains("NativeIngestion", miPolicy.AllowedUsages);
53+
Assert.Contains("ExternalTable", miPolicy.AllowedUsages);
4454
}
4555

4656
[Fact]

KustoSchemaTools/Changes/DatabaseChanges.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +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.Policies?.ManagedIdentity != null && oldState.Policies.ManagedIdentity.Any())
26+
otherFromScripts.Add(ManagedIdentityPolicy.CreateCombinedScript(name, oldState.Policies.ManagedIdentity));
2527
}
2628

2729
var otherToScripts = new List<DatabaseScriptContainer>();
2830
if (newState.Scripts != null)
2931
otherToScripts.AddRange(newState.Scripts.Select(itm => new DatabaseScriptContainer(itm, "DatabaseScript")));
3032
if (newState.DefaultRetentionAndCache != null)
3133
otherToScripts.AddRange(newState.DefaultRetentionAndCache.CreateScripts(name, "database"));
34+
if (newState.Policies?.ManagedIdentity != null && newState.Policies.ManagedIdentity.Any())
35+
otherToScripts.Add(ManagedIdentityPolicy.CreateCombinedScript(name, newState.Policies.ManagedIdentity));
3236

3337
if (otherToScripts.Count > 0)
3438
{

KustoSchemaTools/Model/Database.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ public class Database
3737

3838
public Dictionary<string, FollowerDatabase> Followers { get; set; } = new Dictionary<string, FollowerDatabase>();
3939

40+
public DatabasePolicies Policies { get; set; } = new DatabasePolicies();
41+
4042
public string EscapedName => Name.BracketIfIdentifier();
4143
}
4244
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace KustoSchemaTools.Model
2+
{
3+
public class DatabasePolicies
4+
{
5+
public List<ManagedIdentityPolicy> ManagedIdentity { get; set; } = new List<ManagedIdentityPolicy>();
6+
}
7+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using KustoSchemaTools.Changes;
2+
using Newtonsoft.Json;
3+
using KustoSchemaTools.Helpers;
4+
using KustoSchemaTools.Parser;
5+
6+
namespace KustoSchemaTools.Model
7+
{
8+
public class ManagedIdentityPolicy
9+
{
10+
public string ObjectId { get; set; }
11+
public List<string> AllowedUsages { get; set; } = new List<string>();
12+
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)
18+
{
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();
23+
var json = JsonConvert.SerializeObject(policyObjects, Serialization.JsonPascalCase);
24+
return new DatabaseScriptContainer("ManagedIdentityPolicy", 80, $".alter-merge database {databaseName.BracketIfIdentifier()} policy managed_identity ```{json}```");
25+
}
26+
}
27+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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(Policy)
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+
if (database.Policies == null)
21+
database.Policies = new DatabasePolicies();
22+
database.Policies.ManagedIdentity = rows
23+
.Select(r => new ManagedIdentityPolicy
24+
{
25+
ObjectId = r.ObjectId,
26+
AllowedUsages = r.AllowedUsages
27+
.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
28+
.OrderBy(u => u)
29+
.ToList()
30+
})
31+
.OrderBy(p => p.ObjectId)
32+
.ToList();
33+
}
34+
35+
private class ManagedIdentityRow
36+
{
37+
public string ObjectId { get; set; }
38+
public string AllowedUsages { get; set; }
39+
}
40+
}
41+
}

0 commit comments

Comments
 (0)