PowerShell コマンドレットで ACL を設定する自作関数

<font color="MediumVioletRed">**※実行は自己責任でお願いします。**</font>

## 関数
```PowerShell:PowerShell
function ACLFunc {
  param (
    [Parameter(Position=0)][string]$Path,
    [ValidateCount(2, 2)][bool[]]$FromParentACL,
    [switch]$Remove,
    [switch]$ResetToInheritedAccess,
    [switch]$Help,
    [array]$ValueList,
    [Parameter(ValueFromRemainingArguments=$true)]$ACL
  )
  ### Usage ###
  $description = @'

    Usage: 
      ACLFunc -Help
      ACLFunc -Path <target path>
      ACLFunc -Path <target path> <user name>
      ACLFunc -Path <target path> -ACL <acl>
      ACLFunc -Path <target path> -ACL <acl> -Remove
      ACLFunc -Path <target path> -FromParentACL <bool>, <bool>
      ACLFunc -Path <target path> -ResetToInheritedAccess
      ACLFunc -ValueList <ace field>

    Parameters:
      -Help       ヘルプを表示
      -Path      （選択）ACL を変更する対象のパス
      -ACL       （選択）ACL の定義列記
                   順序 :  User AccessControlType FileSystemRights ( InheritanceFlags PropagationFlags )
      -Remove    （必須）-Path ･ -ACL とともに使用
      -FromParentACL
                 （必須）継承を無効にする場合は true 
                         そのとき継承されたアクセス規則を保持する場合は true 
                   順序 :  <bool isProtected> <bool preserveInheritance>
      -ResetToInheritedAccess
                 （必須）継承された権限のみに再設定・ブール値（所有者は据置）
                   真偽 :  -ResetToInheritedAccess ･ -ResetToInheritedAccess:$true
      -ValueList （必須）Access Control Entry の Field を指定
                   項目 :  AccessControlType FileSystemRights InheritanceFlags PropagationFlags

    Remarks:
      『（選択）オプション 』はオプション名を省略できます。
      『（必須）オプション 』同士は排他的です。

    Example:
      # 特定のユーザーに関する権限を表示
      ACLFunc ./test ユーザー
      # ACL を設定
      ACLFunc -Path ./test -ACL ユーザー,Allow,FullControl
      ACLFunc -Path ./test -ACL ユーザー,Allow,FullControl,"ContainerInherit,ObjectInherit",None
      # 指定した ACL を削除
      ACLFunc -Path ./test -ACL ユーザー,Allow,FullControl -Remove
      ACLFunc -Path ./test -ACL ユーザー,Allow,FullControl,"ContainerInherit,ObjectInherit",None -Remove
      # 継承を無効、継承された権限は保持
      ACLFunc -Path ./test -FromParentACL $true,$true
      # 所有者はそのままに、親から継承されたアクセス権のみに設定
      ACLFunc -Path ./test -ResetToInheritedAccess
      # 各 ACE 項目に設定できる値の表示
      ACLFunc -ValueList AccessControlType,FileSystemRights,InheritanceFlags,PropagationFlags

'@ -replace "(?m)^\x20{4}", ""

  ### Preparation ###
  # TypeAccelerators クラスを取得
  # 各型を TypeAccelerators に登録
  $TypesAliases = [PSCustomObject]@{
    Type    = "System.Management.Automation.TypeAccelerators"
    Aliases = @{
      "System.Security.AccessControl." = @(
        "AccessControlType",
        "FileSystemRights",
        "InheritanceFlags",
        "PropagationFlags",
        "FileSecurity",
        "DirectorySecurity",
        "FileSystemAccessRule"
      )
      "System.Security.Principal." = @(
        "NTAccount",
        "SecurityIdentifier"
      )
    }
  }
  $AliasesAC = $TypesAliases.Aliases["System.Security.AccessControl."]
  $AliasesPL = $TypesAliases.Aliases["System.Security.Principal."]
  $TA = [PSObject].Assembly.GetType($TypesAliases.Type)
  foreach ($context in $TypesAliases.Aliases.Keys) {
    foreach ($alias in $TypesAliases.Aliases[$context]) {
      $TypeObject = [Type]($context + $alias)
      $TA::Add($alias, $TypeObject)
    } 
  }
  $ExitFunction = $false

  # echo 1
  # 引数のチェック
  if ($ValueList) {
    try {
      switch (@($ValueList)) {
        AccessControlType{"[$_]";[enum]::GetNames([AccessControlType]);""}
         FileSystemRights{"[$_]";[enum]::GetNames([FileSystemRights]);""}
         InheritanceFlags{"[$_]";[enum]::GetNames([InheritanceFlags]);""}
         PropagationFlags{"[$_]";[enum]::GetNames([PropagationFlags]);""}
                  default{throw}
      }
      $ExitFunction = $true
      return 
    } 
    catch {
      $ExitFunction = $true
      throw "指定された項目が存在しません。"
    } 
    finally {
      if ($ExitFunction) {
        $AliasesAC.ForEach{[void]$TA::Remove($_)}
        $AliasesPL.ForEach{[void]$TA::Remove($_)}
      }
    }
  } 
  elseif ($Help -or ![bool]$Path) {
    $AliasesAC.ForEach{[void]$TA::Remove($_)}
    $AliasesPL.ForEach{[void]$TA::Remove($_)}
    return $description
  } 
  elseif ([bool]$Path) {
    if (!(Test-Path $Path)) {
      $AliasesAC.ForEach{[void]$TA::Remove($_)}
      $AliasesPL.ForEach{[void]$TA::Remove($_)}
      throw "指定されたパスが存在しません。"
    } 
  }

  function SortACL ($ACLObject, $CfUser) {
    $ReturnValue = $ACLObject |
      Where-Object { 
        $_.IdentityReference.Value.Split('\')[-1] -match $CfUser 
      } | 
        Format-Table -AutoSize -Wrap -Property `
          @{Name="User";Expression={
            ($_.IdentityReference.Value.Split('\')[-1])[0..14] -join ''};Alignment='Left'
          },
          @{Name="IsInherited";      Expression={$_.IsInherited};      Alignment='Left'},
          @{Name="AccessControlType";Expression={$_.AccessControlType};Alignment='Left'},
          @{Name="FileSystemRights"; Expression={$_.FileSystemRights}; Alignment='Left'},
          @{Name="InheritanceFlags"; Expression={$_.InheritanceFlags}; Alignment='Left'},
          @{Name="PropagationFlags"; Expression={$_.PropagationFlags}; Alignment='Left'}
    return $ReturnValue
  }

  ### Process ###
  # $ACL を配列化
  # 要素数を配列番号に変換して出力
  $Enumerable = [System.Linq.Enumerable]
  $DefACL = [System.Collections.ArrayList]::new()
  $Enumerable::Range(0,[Math]::Max(0,$ACL.Count)) | ForEach-Object {
    $NumKey = $_
    $Enumerable::Range(0,[Math]::Max(0,$ACL[$NumKey].Count)) | ForEach-Object {
      [void]$DefACL.Add(@(@($ACL)[$NumKey])[$_])
    }
  }
  $DefACL = [array]$DefACL

  # echo 2
  # 引数 <path> <user> の処理
  if ([bool]$FromParentACL.Count -or $ResetToInheritedAccess) {
    # スキップ
  } 
  elseif (![bool]$DefACL) {
    $CfUser = ".*"
    Write-Output "現在のACLを表示します。"
    $CurrentACL = SortACL (Get-ACL $Path).Access $CfUser
    $AliasesAC.ForEach{[void]$TA::Remove($_)}
    $AliasesPL.ForEach{[void]$TA::Remove($_)}
    return $CurrentACL
  } 
  else {
    try {
      # $IDRef = $DefACL -as [NTAccount]
      $IDRef = [NTAccount]::new(@($DefACL)[0])
      $SID = $IDRef.Translate([SecurityIdentifier])

      $CfUser = @($DefACL)[0].Split('\')[-1]
      if ($DefACL.Count -eq 1) {
        Write-Output "現在のACLを表示します。"
        $CurrentACL = SortACL (Get-ACL $Path).Access $CfUser
        $ExitFunction = $true
        return $CurrentACL
      }
      else {
        $FullyQualifiedUserName = $SID.Translate([NTAccount])
        $DefACL[0] = $FullyQualifiedUserName
      }
    } 
    catch {
      $ExitFunction = $true
      throw "ユーザー名が有効ではありません。"
    }
    finally {
      if ($ExitFunction) {
        $AliasesAC.ForEach{[void]$TA::Remove($_)}
        $AliasesPL.ForEach{[void]$TA::Remove($_)}
      }
    }
  }

  # echo 3
  # ACL の作成
  try {
    $NewACL = [System.Security.AccessControl.FileSystemAccessRule]
    switch ($DefACL.Count) {
      ({$_ -eq 3}) {
        $ErrorTemplateA = @"
          -ACL <acl list> に列記されている順番もしくは値が正しくないか、項目が足りません。
          [AccessControlType] :  {0}
          [FileSystemRights]  :  {1}
"@;     $ErrorMessageA = $ErrorTemplateA -f $false,$false

        $BoolAccessControlType = [enum]::GetNames([AccessControlType]) -notcontains $DefACL[1]
        $BoolFileSystemRights  = ($DefACL[2]-split"\s*,\s*").Where{$_-notin[enum]::GetNames([FileSystemRights])}
        if ($Enumerable::SequenceEqual($BoolFileSystemRights, @())) {
          $BoolFileSystemRights = $false
        } 
        elseif ([string]::IsNullOrEmpty($BoolFileSystemRights)) {
          $BoolFileSystemRights = $true
        }

        $ErrorMessageA = $ErrorTemplateA -f !$BoolAccessControlType,!$BoolFileSystemRights
        if ($BoolAccessControlType -or
            $BoolFileSystemRights) {
          throw
        }

        $SetACL = $NewACL::new($DefACL[0],$DefACL[2],$DefACL[1])
      }
      ({$_ -eq 5}) {
        $ErrorTemplateB = @"
          -ACL <acl list> に列記されている順番もしくは値が正しくないか、項目が足りません。
          [AccessControlType] :  {0}
          [FileSystemRights]  :  {1}
          [InheritanceFlags]  :  {2}
          [PropagationFlags]  :  {3}
"@;     $ErrorMessageB = $ErrorTemplateB -f $false,$false,$false,$false

        $BoolAccessControlType = [enum]::GetNames([AccessControlType]) -notcontains $DefACL[1]
        $BoolFileSystemRights  = ($DefACL[2]-split"\s*,\s*").Where{$_-notin[enum]::GetNames([FileSystemRights])}
        if ($Enumerable::SequenceEqual($BoolFileSystemRights, @())) {
          $BoolFileSystemRights = $false
        } 
        elseif ([string]::IsNullOrEmpty($BoolFileSystemRights)) {
          $BoolFileSystemRights = $true
        }
        $BoolInheritanceFlags  = ($DefACL[3]-split"\s*,\s*").Where{$_-notin[enum]::GetNames([InheritanceFlags])}
        if ($Enumerable::SequenceEqual($BoolInheritanceFlags, @())) {
          $BoolInheritanceFlags = $false
        } 
        elseif ([string]::IsNullOrEmpty($BoolInheritanceFlags)) {
          $BoolInheritanceFlags = $true
        }
        $BoolPropagationFlags  = [enum]::GetNames([PropagationFlags]) -notcontains $DefACL[4]

        $ErrorMessageB = $ErrorTemplateB -f (
          !$BoolAccessControlType,!$BoolFileSystemRights,!$BoolInheritanceFlags,!$BoolPropagationFlags
        )
        if ($BoolAccessControlType -or
            $BoolFileSystemRights -or
            $BoolInheritanceFlags -or
            $BoolPropagationFlags) {
          throw
        }

        $SetACL = $NewACL::new($DefACL[0],$DefACL[2],$DefACL[3],$DefACL[4],$DefACL[1])
      }
      default {
        if ([bool]$FromParentACL.Count -or $ResetToInheritedAccess) {
          break
        }
        throw
      }
    }
  } 
  catch {
    Write-Error (($ErrorMessageA+$ErrorMessageB) -replace "(?m)^\x20{10}", "")
    $ExitFunction = $true
    return    # $description
  } 
  finally {
    if ($ExitFunction) {
      $AliasesAC.ForEach{[void]$TA::Remove($_)}
      $AliasesPL.ForEach{[void]$TA::Remove($_)}
    }
  }

  # echo 4
  # ACL を設定
  $TargetACL = Get-ACL $Path
  if ($ResetToInheritedAccess) {
    $CurrentOwner = $TargetACL.GetOwner([NTAccount])
    if ((Get-Item $Path).PSIsContainer) {
      $ResetACL = [DirectorySecurity]::new()
    } 
    else {
      $ResetACL = [FileSecurity]::new()
    }
    $ResetACL.SetOwner($CurrentOwner)
    $ResetACL.SetAccessRuleProtection($false, $true)
    if ([bool]$SetACL) {
      $ResetACL.AddAccessRule($SetACL)
    }
    Set-ACL -Path $Path -ACLObject $ResetACL
  } 
  elseif ([bool]$FromParentACL.Count) {
    if ([bool]$SetACL) {
      $TargetACL.SetAccessRule($SetACL)
    }
    $TargetACL.SetAccessRuleProtection($FromParentACL[0],$FromParentACL[-1])
    $TargetACL | Set-ACL $Path

    # アクセス制御リストを正規順に並び替え
    $TestACL = Get-ACL $Path
    if (!($TestACL).AreAccessRulesCanonical) {
      $TestOwner = $TestACL.GetOwner([NTAccount])
      if ((Get-Item $Path).PSIsContainer) {
        $RecastACL = [DirectorySecurity]::new()
      } 
      else {
        $RecastACL = [FileSecurity]::new()
      }
      
      $RecastACL.SetOwner($TestOwner)
      $RecastACL.SetAccessRuleProtection($FromParentACL[0],$FromParentACL[-1])

      $TestACL.Access | 
        Where-Object { !$_.IsInherited -and $_.AccessControlType -eq "Deny" } | 
          ForEach-Object { $RecastACL.AddAccessRule($_) }
      $TestACL.Access | 
        Where-Object { !$_.IsInherited -and $_.AccessControlType -eq "Allow" } | 
          ForEach-Object { $RecastACL.AddAccessRule($_) }
      $TestACL.Access | 
        Where-Object { $_.IsInherited } | 
          ForEach-Object { $RecastACL.AddAccessRule($_) }
          
      Set-ACL -Path $Path -ACLObject $RecastACL
    }
  } 
  elseif ($Remove) {
    $TargetACL.RemoveAccessRuleSpecific($SetACL)
    $TargetACL | Set-ACL $Path
  } 
  else {
    $TargetACL.SetAccessRule($SetACL)
    $TargetACL | Set-ACL $Path
  }

  Write-Output "修正したALCを出力します。"
  $ModifiedACL = SortACL (Get-ACL $Path).Access $CfUser
  $AliasesAC.ForEach{[void]$TA::Remove($_)}
  $AliasesPL.ForEach{[void]$TA::Remove($_)}
  return $ModifiedACL
}
```

## 詳細
### 現在のアクセス権の確認
- **権限確認対象のパスを保持**
`$Path = "<target path>"`

- **現在のアクセス権を出力**
`(Get-ACL $Path).Access`

### 特定のアクセス権を変更する
- **権限操作の対象パスを変数に格納**
`$Path = "<target path>"`

- **既存のアクセス権構造を取得**
`$TargetACL = Get-ACL $Path`

- **新しいアクセス権の準備**
```PowerShell
# 名前空間
using namespace System.Security.AccessControl

# 新しいアクセス権の準備（以下のどちらでもよい）
$SetACL = [FileSystemAccessRule]::new("<user>","<file system rights>","<access control type>")
$SetACL = [FileSystemAccessRule]::new("<user>","<file system rights>","<inheritance flags>","<propagation flags>","<access control type>")
# 設定できる値は以下の通り
[enum]::GetNames([FileSystemRights])
[enum]::GetNames([InheritanceFlags])
[enum]::GetNames([PropagationFlags])
[enum]::GetNames([AccessControlType])
```

- **新しいアクセス権に変更**
`$TargetACL.SetAccessRule($SetACL)`

- **指定したアクセス権の削除**
`$TargetACL.RemoveAccessRuleSpecific($SetACL)`

- **設定の反映**
`$TargetACL | Set-ACL $Path`

### 既存のアクセス権を丸ごと置き換える
- **権限操作の対象パスを変数に格納**
`$Path = "<target path>"`

- **ディレクトリまたはファイル用にオブジェクトを用意**
```PowerShell
# 名前空間
using namespace System.Security.AccessControl

if ((Get-Item $Path).PSIsContainer) {
    $ResetACL = [DirectorySecurity]::new()
} else {
    $ResetACL = [FileSecurity]::new()
}
```

- **所有権の設定**
```PowerShell
# 名前空間
using namespace System.Security.Principal

# IdentityReference のセット
$NewOwner = [NTAccount]"<user>"
$ResetACL.SetOwner($NewOwner)
```

- **継承の有無**
```PowerShell
# 配列の一番目は「継承を無効化するかどうか」、
# 二番目は一番目が $true であるとき「継承されるアクセス権を保持するかどうか」
[bool[]]$BoolOnly = $false,$true
$ResetACL.SetAccessRuleProtection($BoolOnly[0],$BoolOnly[-1])
```

- **追加するルールの準備**
```PowerShell
# どっちでも
$SetACL = [FileSystemAccessRule]::new("<user>","<access control type>","<file system rights>")
$SetACL = [FileSystemAccessRule]::new("<user>","<access control type>","<file system rights>","<inheritance flags>","<propagation flags>")
```

- **新しいアクセス権の追加**
`$ResetACL.AddAccessRule($SetACL)`

- **設定の反映**
`$ResetACL | Set-ACL $Path`

### 継承の無効及び継承されているアクセス権の保持
- **権限操作の対象パスを変数に格納**
`$Path = "<target path>"`

- **既存のアクセス権構造を取得**
`$TargetACL = Get-ACL $Path`

- **継承の無効と継承されているアクセス権の保持**
```PowerShell
# 配列の一番目は「継承を無効化するかどうか」、
# 二番目は一番目が $true であるとき「継承されているアクセス権を保持するかどうか」
[bool[]]$BoolOnly = $true,$true
$TargetACL.SetAccessRuleProtection($BoolOnly[0],$BoolOnly[-1])
```

- **設定の反映**
`$TargetACL | Set-ACL $Path`

### 「このアクセス制御リストは標準の形式ではないため、変更できません。」の解決
```PowerShell
# 各々で値を設定
$Path = "<target path>"
[bool[]]$BoolOnly = $false,$true

# ACL が正規の順序でなければ修正
using namespace System.Security.Principal
using namespace System.Security.AccessControl
$CurrentACL = Get-ACL $Path
if (!($CurrentACL).AreAccessRulesCanonical) {
    $CurrentOwner = $CurrentACL.GetOwner([NTAccount])
    if ((Get-Item $Path).PSIsContainer) {
        $RecastACL = [DirectorySecurity]::new()
    } else {
        $RecastACL = [FileSecurity]::new()
    }
    $RecastACL.SetOwner($CurrentOwner)
    $RecastACL.SetAccessRuleProtection($BoolOnly[0],$BoolOnly[-1])
    $CurrentACL.Access | 
        Where-Object { !$_.IsInherited -and $_.AccessControlType -eq "Deny" } | 
            ForEach-Object { $RecastACL.AddAccessRule($_) }
    $CurrentACL.Access | 
        Where-Object { !$_.IsInherited -and $_.AccessControlType -eq "Allow" } | 
            ForEach-Object { $RecastACL.AddAccessRule($_) }
    $CurrentACL.Access | 
        Where-Object { $_.IsInherited } | 
            ForEach-Object { $RecastACL.AddAccessRule($_) }
    Set-ACL -Path $Path -ACLObject $RecastACL
}
```

## 引用・参考
- [Getting "Incorrectly ordered" error message when setting a folder permission to a domain group using Powershell](https://stackoverflow.com/questions/62623764/getting-incorrectly-ordered-error-message-when-setting-a-folder-permission-to)