Private/datalake-acls.ps1

using namespace Azure.Storage.Files.DataLake.Models

$script:Global_ctxStorageAccountName = ""
$script:Global_ctxContainerName = ""
$script:Global_ctx = ""

function Get-RKStorageContext {
    [CmdletBinding()]
    param([string]$StorageAccountName,
        [string]$ContainerName)

    #Wrap call in $Null to prevent any additional piped output from being sent back as context.
    $Null = @(
        # If the storage account in use has changed update the Context
        if (($script:Global_ctxStorageAccountName -ne $StorageAccountName) `
                -or ($script:Global_ctxContainerName -ne $ContainerName)) {
            # Flush the cache if changing
            Update-RKLakeFromCache

            $script:Temp_ctx = New-AzStorageContext -StorageAccountName $StorageAccountName -UseConnectedAccount
            
            # Redirect stderr to $null - we handle the error via the ErrorVariable instead.
            ( Get-AzStorageContainer -Context $script:Temp_ctx -Name $ContainerName -ErrorVariable storageErr ) 2> $null

            if ($storageErr){
                throw $storageErr
            }

            $script:Global_ctx = $script:Temp_ctx
            $script:Global_ctxStorageAccountName = $StorageAccountName
            $script:Global_ctxContainerName = $ContainerName
            Reset-RKCache
        }
    )
    return $script:Global_ctx
}




function Get-RKLakePathAcl {
    [CmdletBinding()]
    param (
        [string]$Path,
        [string]$ContainerName,
        $ctx
    )
    
    if ("" -eq $Path) {
        $Path = '/'
    }
    if ($script:aclCache.ContainsKey($Path)) {
        return $script:aclCache[$Path]
    }
    elseif ($Path -eq '/') {
        $acl = (Get-AzDataLakeGen2Item -Context $ctx -FileSystem $ContainerName).ACL
    }
    else {
        $acl = (Get-AzDataLakeGen2Item -Context $ctx -FileSystem $ContainerName -Path $Path).ACL
    }
    $script:aclCache[$Path] = $acl
    return $acl
}


function Test-RKLakePermission {
    [CmdletBinding()]
    param(
        [string]$Path,
        [string]$AccessControlType,
        [string]$EntityId,
        [RolePermissions]$Permissions,
        [bool]$DefaultScope = $False,
        [string]$ContainerName,
        $ctx
    )
    $Current = Get-RKLakePathAcl -Path $Path -ContainerName $ContainerName -ctx $ctx
    $MatchedAcls = $Current | Where-Object { (($_.AccessControlType -eq $AccessControlType) `
                -and ($_.EntityId -eq $EntityId ) `
                -and ($_.DefaultScope -eq $DefaultScope ) 
        ) }

    if (-not $MatchedAcls) {
        return $false
    }
    else {
        return (($MatchedAcls.Permissions) -eq $Permissions)
    }
    
}

function ConvertTo-RKRolePermissions {
    [CmdletBinding()]
    param($TargetPermissions)

    if ($TargetPermissions.Length -gt 0) {
        $TargetPermissions = $TargetPermissions.replace("r", 'Read,').replace("w", 'Write,').replace("x", 'Execute,')
        $TargetPermissions = $TargetPermissions.Substring(0, $TargetPermissions.Length - 1)
        $TargetPermissions = [RolePermissions]$TargetPermissions
    }
    else {
        $TargetPermissions = [RolePermissions]::None
    }
    return $TargetPermissions
}

function ConvertTo-RKPermissionString {
    [CmdletBinding()]
    param(
        [RolePermissions]$Permissions
    )
    
    if (($Permissions -band 'Read') -eq 'Read') {
        $Res = "r"
    } 
    else {
        $Res = "-"
    }
    if (($Permissions -band 'Write') -eq 'Write') {
        $Res = $Res + "w"
    } 
    else {
        $Res = $Res + "-"
    }
    if (($Permissions -band 'Execute') -eq 'Execute') {
        $Res = $Res + "x"
    } 
    else {
        $Res = $Res + "-"
    }
    return $Res
}

function Get-RKUpperPath {
    [CmdletBinding()]
    param([string]$Path)
    if (-not $Path.Contains('/')) {
        return $null
    }
    $Paths = $path.split('/')
    return $Paths[0..($Paths.Count - 2)] -join '/'
}


function Add-RKLakePathAcl {
    [CmdletBinding()]
    param(
        [string]$Path,
        [string]$EntityId,
        [string]$PrincipalType,
        [string]$Permissions,
        [AclPermissionType]$AclPermissionType,
        [bool]$ApplyMinimumPermissionsToParentFolders = $true,
        [bool]$ForceApplyPermission = $true,
        [bool]$ApplyToSubFolders = $true,
        [string]$ContainerName,
        $ctx
    )
    $Path = $Path.Trim("/")
    $PermissionsObj = ConvertTo-RKRolePermissions $Permissions
    $PermissionsStr = ConvertTo-RKPermissionString $PermissionsObj

    $DefaultPermission = ($AclPermissionType -eq [AclPermissionType]::Default)
    
    $Exists = Test-RKLakePermission -ContainerName $ContainerName -ctx $ctx -Path $Path -AccessControlType $PrincipalType -EntityId $EntityId -Permissions $PermissionsObj -DefaultScope $DefaultPermission

    # If we are force applying permissions or if the permission doesn't exist, apply them.
    if ($ForceApplyPermission -or (-not $Exists)) {
        # Applying permissions to subfolders
        if ($ApplyToSubFolders) {
            if ($Path -eq "") { $Path = '/' }
            if ($script:recursiveAclsToApply.ContainsKey($Path)) {
                $acl = $script:recursiveAclsToApply[$Path]
                $acl = Set-AzDataLakeGen2ItemAclObject -AccessControlType $PrincipalType -Permission $PermissionsStr -EntityId $EntityId -DefaultScope:$DefaultPermission -InputObject $acl
            }
            else {
                $acl = Set-AzDataLakeGen2ItemAclObject -AccessControlType $PrincipalType -Permission $PermissionsStr -EntityId $EntityId -DefaultScope:$DefaultPermission
            }
            $script:recursiveAclsToApply[$path] = $acl
        }
        else {
            if ($Path -eq "") { $Path = '/' }
            $script:aclCache[$Path] = Set-AzDataLakeGen2ItemAclObject -AccessControlType $PrincipalType -EntityID $EntityId -Permission $PermissionsStr -InputObject $script:aclCache[$path] -DefaultScope:$DefaultPermission
            $script:changedPaths += $Path
        }
    }

    #If we need to apply minimum parent permissions loop through script again to provide basic r-x access on the parent path.
    if (($ApplyMinimumPermissionsToParentFolders) -and ($Path -notin "", '/')) {
        $Path = Get-RKUpperPath -Path $Path

        Add-RKLakePathAcl -Path $Path -EntityId $EntityId -PrincipalType $PrincipalType `
        -Permissions "rx" -AclPermissionType Access -ApplyMinimumPermissionsToParentFolders $True `
        -ForceApplyPermission $ForceApplyPermission -ApplyToSubFolders $False `
        -ContainerName $ContainerName -ctx $ctx
    }
    
}

function Test-RKFolderExists {
    [CmdletBinding()]
    param(
        $Path,
        [string]$ContainerName,
        $ctx
    )
    if ($Path -ne '/') {
        return Optimize-RKGetAzDataLakeGen2ItemCommand -ScriptBlock { Get-AzDataLakeGen2Item -Context $ctx -FileSystem $ContainerName -Path $Path }
    }
    else {
        return Optimize-RKGetAzDataLakeGen2ItemCommand -ScriptBlock { Get-AzDataLakeGen2Item -Context $ctx -FileSystem $ContainerName }
    }
}

function New-RKFolder {
    [CmdletBinding()]
    param(
        $Path,
        [string]$ContainerName,
        $ctx
    )
    $dir = New-AzDataLakeGen2Item -Context $ctx -FileSystem $ContainerName -Path $path -Directory
}


function Set-RKLakePathAcl {
    [CmdletBinding()]
    param(
        $ctx,
        [PSCustomObject]$Permission
    )
    $EntityId = Get-RKAADObjectId -ObjectType $Permission.principalType -DisplayName $Permission.principalName

    if (-not (Test-RKFolderExists -Path $Permission.path -ctx $ctx -ContainerName $Permission.containerName)) {
        New-RKFolder -Path $Permission.path -ctx $ctx -ContainerName $Permission.containerName
    }

    if ($Permission.principalType -eq "Group") {
        $PrincipalType = "group"
    }
    else {
        $PrincipalType = "user"
    }

    # apply defaults to non-mandatory settings
    [bool]$ApplyMinimumPermissionsToParentFolders = $Permission.applyMinimumPermissionsToParentFolders ?? $true
    [bool]$ApplyToSubFolders = $Permission.applyToSubFolders ?? $true 
    [bool]$ForceApplyPermission = $Permission.forceApplyPermission ?? $true
    [AclPermissionType]$AclPermissionType = $Permission.aclType ?? [AclPermissionType]::Access

    # Apply Access Permissions
    if ($AclPermissionType -in [AclPermissionType]::Access, [AclPermissionType]::Both){
        Add-RKLakePathAcl -Path $Permission.path -EntityId $EntityId -PrincipalType $PrincipalType `
        -Permissions $Permission.permission -AclPermissionType Access -ApplyMinimumPermissionsToParentFolders $ApplyMinimumPermissionsToParentFolders `
        -ForceApplyPermission $ForceApplyPermission -ApplyToSubFolders $ApplyToSubFolders `
        -ContainerName $Permission.containerName -ctx $ctx
    }

    # Apply Default Permissions
    if ($AclPermissionType -in [AclPermissionType]::Default, [AclPermissionType]::Both){
        Add-RKLakePathAcl -Path $Permission.path -EntityId $EntityId -PrincipalType $PrincipalType `
        -Permissions $Permission.permission -AclPermissionType Default -ApplyMinimumPermissionsToParentFolders $ApplyMinimumPermissionsToParentFolders `
        -ForceApplyPermission $ForceApplyPermission -ApplyToSubFolders $ApplyToSubFolders `
        -ContainerName $Permission.containerName -ctx $ctx
    }
}

function Reset-RKCache {
    [CmdletBinding()]
    $script:aclCache = @{}
    $script:recursiveAclsToApply = @{}
    $script:changedPaths = @()
}

function Update-RKLakeFromCache {
    # Parameter help description
    $ContainerName = $script:Global_ctxContainerName
    $ctx = $script:Global_ctx
    
    foreach ($Path in $script:changedPaths) {
        $acl = $script:aclCache[$Path]
        Write-Host "Updating $Path Single ACL"
        if ($Path -eq "/") {
            Update-AzDataLakeGen2Item -Context $ctx -FileSystem $ContainerName -Acl $acl
        }
        else {
            Update-AzDataLakeGen2Item -Context $ctx -FileSystem $ContainerName -Path $Path -Acl $acl
        }
    }

    foreach ($Path in $script:recursiveAclsToApply.Keys) {
        $acl = $script:recursiveAclsToApply[$Path]
        Write-Host "Updating $Path Recursively"
        if ($Path -eq "/") {
            Update-AzDataLakeGen2AclRecursive  -Context $ctx -FileSystem $ContainerName -Acl $acl
        }
        else {
            Update-AzDataLakeGen2AclRecursive  -Context $ctx -FileSystem $ContainerName -Path $Path -Acl $acl
        }   
    }

    Reset-RKCache
}

Function Optimize-RKGetAzDataLakeGen2ItemCommand {
    param(
        [ScriptBlock]$ScriptBlock
    )

    # clean out error automatic variable
    $Error.Clear()

    # invoke Get-AzDataLakeGen2Item script, suppress any messages to stderr
    (Invoke-Command -ScriptBlock $ScriptBlock -ErrorVariable errmsg) 2> $null

    $e = $errmsg[0].exception | Select-Object Status, ErrorCode, Message

    if ($null -eq $e) {
        # if no error, return function successfully.
        return $true
    }
    elseif ($e.Message -like "*does not exist*") {
        # when the target path does not exist note in logs, clear error and return false.
        Write-Warning "Target path does not exist."
        $Error.Clear()
        return $false
    }
    elseif ($e.Status -eq 403) {
        # if a 403 error is returned there is a permissions / auth issue - an error is raised
        $errMsg = "Authorization error, ensure the following:" `
        + "`n- If the storage account is behind a firewall or private endpoint that you can access the resource from this location." `
        + "`n- The account being used to authenticate with the storage account has appropriate permissions:" `
        + "`n`t- If you are using a Service Principal ensure it has been assigned the Storage Blob Data Owner role in the scope of the either the target container, parent resource group or subscription." `
        + "`n`t- If you are connecting with a user / AD account, ensure you are the owning user of the target container or directory to which you plan to apply ACL settings. To set ACLs recursively, this includes all child items in the target container or directory."
        Write-RKError $errMsg
        throw $e
    }
    elseif ($e.Message -like "*no such host is known*") {
        Write-RKError "The storage account defined in the permission does not exist - review stack trace for further information."
        throw $e
    }
    else {
        Write-RKError "Uncategorized error - review stack trace for further information."
        throw $e
    }
}

enum AclPermissionType {
    Access = 1
    Default = 2
    Both = 4
}