Skip to content

Commit 4627aa2

Browse files
WIP repro 62148 in code
1 parent 5274e8f commit 4627aa2

File tree

1 file changed

+99
-24
lines changed

1 file changed

+99
-24
lines changed

src/Servers/IIS/IIS/test/IIS.ShadowCopy.Tests/ShadowCopyTests.cs

Lines changed: 99 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,64 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Security.AccessControl;
5+
using System.Security.Principal;
46
using Microsoft.AspNetCore.InternalTesting;
57
using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities;
8+
using Microsoft.AspNetCore.Server.IntegrationTesting.IIS;
69

710
namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests;
811

912
[Collection(PublishedSitesCollection.Name)]
10-
public class ShadowCopyTests : IISFunctionalTestBase
13+
public class ShadowCopyTests(PublishedSitesFixture fixture) : IISFunctionalTestBase(fixture)
1114
{
12-
public ShadowCopyTests(PublishedSitesFixture fixture) : base(fixture)
15+
16+
public bool IsDirectoryEmpty(string path)
1317
{
18+
return !Directory.EnumerateFileSystemEntries(path).Any();
19+
}
20+
21+
public static NTAccount DefaultAppPoolAccount { get; } = new NTAccount("IIS AppPool", "DefaultAppPool");
22+
23+
[ConditionalFact]
24+
public async Task ShadowCopy_CopyFailsWithUsefulExceptionMessage_WhenNoPermissionsToShadowCopyDirectory()
25+
{
26+
// Arrange
27+
using var shadowCopyDirectory = TempDirectory.CreateWithNoPermissions(DefaultAppPoolAccount);
28+
var deploymentParameters = Fixture.GetBaseDeploymentParameters();
29+
deploymentParameters.HandlerSettings["enableShadowCopy"] = "true";
30+
deploymentParameters.HandlerSettings["shadowCopyDirectory"] = shadowCopyDirectory.DirectoryPath;
31+
32+
deploymentParameters.ServerConfigActionList.Add((config, _) =>
33+
{
34+
var appPools = config.RequiredElement("system.applicationHost").RequiredElement("applicationPools");
35+
36+
var defaultAppPool = appPools.Elements("add")
37+
.FirstOrDefault(m => m.Attribute("name")?.Value == "DefaultAppPool");
38+
39+
Assert.NotNull(defaultAppPool);
40+
41+
defaultAppPool.RequiredElement("processModel")
42+
.SetAttributeValue("identityType", "ApplicationPoolIdentity");
43+
});
44+
45+
var deploymentResult = await DeployAsync(deploymentParameters);
46+
47+
// Act
48+
var response = await deploymentResult.HttpClient.GetAsync("Wow!");
49+
50+
// Assert
51+
Assert.False(response.IsSuccessStatusCode);
52+
Assert.True(IsDirectoryEmpty(shadowCopyDirectory.DirectoryPath), "Expected shadow copy shadowCopyDirectory to be empty");
1453
}
1554

1655
[ConditionalFact]
1756
public async Task ShadowCopyDoesNotLockFiles()
1857
{
19-
using var directory = TempDirectory.Create();
58+
using var shadowCopyDirectory = TempDirectory.Create();
2059
var deploymentParameters = Fixture.GetBaseDeploymentParameters();
2160
deploymentParameters.HandlerSettings["enableShadowCopy"] = "true";
22-
deploymentParameters.HandlerSettings["shadowCopyDirectory"] = directory.DirectoryPath;
61+
deploymentParameters.HandlerSettings["shadowCopyDirectory"] = shadowCopyDirectory.DirectoryPath;
2362

2463
var deploymentResult = await DeployAsync(deploymentParameters);
2564
var response = await deploymentResult.HttpClient.GetAsync("Wow!");
@@ -42,14 +81,17 @@ public async Task ShadowCopyDoesNotLockFiles()
4281
[ConditionalFact]
4382
public async Task ShadowCopyRelativeInSameDirectoryWorks()
4483
{
84+
// Arrange
4585
var directoryName = Path.GetRandomFileName();
4686
var deploymentParameters = Fixture.GetBaseDeploymentParameters();
4787
deploymentParameters.HandlerSettings["enableShadowCopy"] = "true";
4888
deploymentParameters.HandlerSettings["shadowCopyDirectory"] = directoryName;
4989

90+
// Act
5091
var deploymentResult = await DeployAsync(deploymentParameters);
5192
var response = await deploymentResult.HttpClient.GetAsync("Wow!");
5293

94+
// Assert
5395
Assert.True(response.IsSuccessStatusCode);
5496
var directoryInfo = new DirectoryInfo(deploymentResult.ContentRoot);
5597

@@ -81,7 +123,7 @@ public async Task ShadowCopyRelativeOutsideDirectoryWorks()
81123
var deploymentResult = await DeployAsync(deploymentParameters);
82124
var response = await deploymentResult.HttpClient.GetAsync("Wow!");
83125

84-
// Check if directory can be deleted.
126+
// Check if shadowCopyDirectory can be deleted.
85127
// Can't delete the folder but can delete all content in it.
86128

87129
Assert.True(response.IsSuccessStatusCode);
@@ -157,7 +199,7 @@ public async Task ShadowCopyDeleteFolderDuringShutdownWorks()
157199
await AssertAppOffline(deploymentResult);
158200

159201
// Delete folder + file after app is shut down
160-
// Testing specific path on startup where we compare the app directory contents with the shadow copy directory
202+
// Testing specific path on startup where we compare the app shadowCopyDirectory contents with the shadow copy shadowCopyDirectory
161203
Directory.Delete(deleteDirPath, recursive: true);
162204

163205
RemoveAppOffline(deploymentResult.ContentRoot);
@@ -171,13 +213,13 @@ public async Task ShadowCopyDeleteFolderDuringShutdownWorks()
171213
[ConditionalFact]
172214
public async Task ShadowCopyE2EWorksWithFolderPresent()
173215
{
174-
using var directory = TempDirectory.Create();
216+
using var shadowCopyDirectory = TempDirectory.Create();
175217
var deploymentParameters = Fixture.GetBaseDeploymentParameters();
176218
deploymentParameters.HandlerSettings["enableShadowCopy"] = "true";
177-
deploymentParameters.HandlerSettings["shadowCopyDirectory"] = directory.DirectoryPath;
219+
deploymentParameters.HandlerSettings["shadowCopyDirectory"] = shadowCopyDirectory.DirectoryPath;
178220
var deploymentResult = await DeployAsync(deploymentParameters);
179221

180-
DirectoryCopy(deploymentResult.ContentRoot, Path.Combine(directory.DirectoryPath, "0"), copySubDirs: true);
222+
DirectoryCopy(deploymentResult.ContentRoot, Path.Combine(shadowCopyDirectory.DirectoryPath, "0"), copySubDirs: true);
181223

182224
var response = await deploymentResult.HttpClient.GetAsync("Wow!");
183225
Assert.True(response.IsSuccessStatusCode);
@@ -216,18 +258,18 @@ public async Task ShadowCopyE2EWorksWithOldFoldersPresent()
216258
DirectoryCopy(secondTempDir.DirectoryPath, deploymentResult.ContentRoot, copySubDirs: true);
217259

218260
response = await deploymentResult.HttpClient.GetAsync("Wow!");
219-
Assert.False(Directory.Exists(Path.Combine(directory.DirectoryPath, "0")), "Expected 0 shadow copy directory to be skipped");
261+
Assert.False(Directory.Exists(Path.Combine(directory.DirectoryPath, "0")), "Expected 0 shadow copy shadowCopyDirectory to be skipped");
220262

221263
// Depending on timing, this could result in a shutdown failure, but sometimes it succeeds, handle both situations
222264
if (!response.IsSuccessStatusCode)
223265
{
224266
Assert.True(response.ReasonPhrase == "Application Shutting Down" || response.ReasonPhrase == "Server has been shutdown");
225267
}
226268

227-
// This shutdown should trigger a copy to the next highest directory, which will be 2
269+
// This shutdown should trigger a copy to the next highest shadowCopyDirectory, which will be 2
228270
await deploymentResult.AssertRecycledAsync();
229271

230-
Assert.True(Directory.Exists(Path.Combine(directory.DirectoryPath, "2")), "Expected 2 shadow copy directory");
272+
Assert.True(Directory.Exists(Path.Combine(directory.DirectoryPath, "2")), "Expected 2 shadow copy shadowCopyDirectory");
231273

232274
response = await deploymentResult.HttpClient.GetAsync("Wow!");
233275
Assert.True(response.IsSuccessStatusCode);
@@ -258,25 +300,25 @@ public async Task ShadowCopyCleansUpOlderFolders()
258300
DirectoryCopy(secondTempDir.DirectoryPath, deploymentResult.ContentRoot, copySubDirs: true);
259301

260302
response = await deploymentResult.HttpClient.GetAsync("Wow!");
261-
Assert.False(Directory.Exists(Path.Combine(directory.DirectoryPath, "0")), "Expected 0 shadow copy directory to be skipped");
303+
Assert.False(Directory.Exists(Path.Combine(directory.DirectoryPath, "0")), "Expected 0 shadow copy shadowCopyDirectory to be skipped");
262304

263305
// Depending on timing, this could result in a shutdown failure, but sometimes it succeeds, handle both situations
264306
if (!response.IsSuccessStatusCode)
265307
{
266308
Assert.True(response.ReasonPhrase == "Application Shutting Down" || response.ReasonPhrase == "Server has been shutdown");
267309
}
268310

269-
// This shutdown should trigger a copy to the next highest directory, which will be 11
311+
// This shutdown should trigger a copy to the next highest shadowCopyDirectory, which will be 11
270312
await deploymentResult.AssertRecycledAsync();
271313

272-
Assert.True(Directory.Exists(Path.Combine(directory.DirectoryPath, "11")), "Expected 11 shadow copy directory");
314+
Assert.True(Directory.Exists(Path.Combine(directory.DirectoryPath, "11")), "Expected 11 shadow copy shadowCopyDirectory");
273315

274316
response = await deploymentResult.HttpClient.GetAsync("Wow!");
275317
Assert.True(response.IsSuccessStatusCode);
276318

277319
// Verify old directories were cleaned up
278-
Assert.False(Directory.Exists(Path.Combine(directory.DirectoryPath, "1")), "Expected 1 shadow copy directory to be deleted");
279-
Assert.False(Directory.Exists(Path.Combine(directory.DirectoryPath, "3")), "Expected 3 shadow copy directory to be deleted");
320+
Assert.False(Directory.Exists(Path.Combine(directory.DirectoryPath, "1")), "Expected 1 shadow copy shadowCopyDirectory to be deleted");
321+
Assert.False(Directory.Exists(Path.Combine(directory.DirectoryPath, "3")), "Expected 3 shadow copy shadowCopyDirectory to be deleted");
280322
}
281323

282324
[ConditionalFact]
@@ -312,6 +354,7 @@ public async Task ShadowCopyIgnoresItsOwnDirectoryWithRelativePathSegmentWhenCop
312354
[ConditionalFact]
313355
public async Task ShadowCopyIgnoresItsOwnDirectoryWhenCopying()
314356
{
357+
// Arrange
315358
using var directory = TempDirectory.Create();
316359
var deploymentParameters = Fixture.GetBaseDeploymentParameters();
317360
deploymentParameters.HandlerSettings["enableShadowCopy"] = "true";
@@ -320,7 +363,10 @@ public async Task ShadowCopyIgnoresItsOwnDirectoryWhenCopying()
320363

321364
DirectoryCopy(deploymentResult.ContentRoot, Path.Combine(directory.DirectoryPath, "0"), copySubDirs: true);
322365

366+
// Act
323367
var response = await deploymentResult.HttpClient.GetAsync("Wow!");
368+
369+
// Assert
324370
Assert.True(response.IsSuccessStatusCode);
325371

326372
using var secondTempDir = TempDirectory.Create();
@@ -341,25 +387,54 @@ public async Task ShadowCopyIgnoresItsOwnDirectoryWhenCopying()
341387

342388
public class TempDirectory : IDisposable
343389
{
390+
private readonly bool _noPermissions;
391+
344392
public static TempDirectory Create()
345393
{
346394
var directoryPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
347395
var directoryInfo = Directory.CreateDirectory(directoryPath);
348396
return new TempDirectory(directoryInfo);
349397
}
350398

351-
public TempDirectory(DirectoryInfo directoryInfo)
399+
public static TempDirectory CreateWithNoPermissions(NTAccount accountToRestrict)
352400
{
353-
DirectoryInfo = directoryInfo;
401+
var directoryPath = Path.Combine($"{Path.GetTempPath()}NoPermissions", Path.GetRandomFileName());
402+
var directoryInfo = Directory.CreateDirectory(directoryPath);
403+
RemovePermissions(directoryInfo, accountToRestrict);
404+
return new TempDirectory(directoryInfo, true);
405+
}
354406

407+
public TempDirectory(DirectoryInfo directoryInfo, bool noPermissions = false)
408+
{
409+
_noPermissions = noPermissions;
355410
DirectoryPath = directoryInfo.FullName;
411+
DirectoryInfo = directoryInfo;
412+
}
413+
414+
private static void RemovePermissions(DirectoryInfo directoryInfo, NTAccount accountToRestrict)
415+
{
416+
var directorySecurity = directoryInfo.GetAccessControl();
417+
418+
directorySecurity.PurgeAccessRules(accountToRestrict);
419+
directoryInfo.SetAccessControl(directorySecurity);
420+
}
421+
422+
private static void RestorePermissions(DirectoryInfo directoryInfo, NTAccount accountToRestore)
423+
{
424+
var directorySecurity = directoryInfo.GetAccessControl();
425+
directorySecurity.AddAccessRule(new FileSystemAccessRule(accountToRestore,
426+
FileSystemRights.FullControl,
427+
InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit,
428+
PropagationFlags.None,
429+
AccessControlType.Allow));
356430
}
357431

358432
public string DirectoryPath { get; }
359433
public DirectoryInfo DirectoryInfo { get; }
360434

361435
public void Dispose()
362436
{
437+
if (_noPermissions) RestorePermissions(DirectoryInfo, DefaultAppPoolAccount);
363438
DeleteDirectory(DirectoryPath);
364439
}
365440

@@ -384,30 +459,30 @@ private static void DeleteDirectory(string directoryPath)
384459
}
385460
catch (Exception e)
386461
{
387-
Console.WriteLine($@"Failed to delete directory {directoryPath}: {e.Message}");
462+
Console.WriteLine($@"Failed to delete shadowCopyDirectory {directoryPath}: {e.Message}");
388463
}
389464
}
390465
}
391466

392467
// copied from https://learn.microsoft.com/dotnet/standard/io/how-to-copy-directories
393468
private static void DirectoryCopy(string sourceDirName, string destDirName, bool copySubDirs, string ignoreDirectory = "")
394469
{
395-
// Get the subdirectories for the specified directory.
470+
// Get the subdirectories for the specified shadowCopyDirectory.
396471
DirectoryInfo dir = new DirectoryInfo(sourceDirName);
397472

398473
if (!dir.Exists)
399474
{
400475
throw new DirectoryNotFoundException(
401-
"Source directory does not exist or could not be found: "
476+
"Source shadowCopyDirectory does not exist or could not be found: "
402477
+ sourceDirName);
403478
}
404479

405480
DirectoryInfo[] dirs = dir.GetDirectories();
406481

407-
// If the destination directory doesn't exist, create it.
482+
// If the destination shadowCopyDirectory doesn't exist, create it.
408483
Directory.CreateDirectory(destDirName);
409484

410-
// Get the files in the directory and copy them to the new location.
485+
// Get the files in the shadowCopyDirectory and copy them to the new location.
411486
FileInfo[] files = dir.GetFiles();
412487
foreach (FileInfo file in files)
413488
{

0 commit comments

Comments
 (0)