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 ;
46using Microsoft . AspNetCore . InternalTesting ;
57using Microsoft . AspNetCore . Server . IIS . FunctionalTests . Utilities ;
8+ using Microsoft . AspNetCore . Server . IntegrationTesting . IIS ;
69
710namespace 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