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