Carbon.Accounts/Carbon.Accounts.psm1

# Copyright WebMD Health Services
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License

using namespace System.ComponentModel
using namespace System.Runtime.InteropServices
using namespace System.Security.Principal

#Requires -Version 5.1
Set-StrictMode -Version 'Latest'

# Functions should use $script:moduleRoot as the relative root from which to find
# things. A published module has its function appended to this file, while a
# module in development has its functions in the Functions directory.
$script:moduleRoot = $PSScriptRoot
$modulesroot = Join-Path -Path $script:moduleRoot -ChildPath 'Modules' -Resolve

Import-Module -Name (Join-Path -Path $modulesRoot -ChildPath 'PureInvoke' -Resolve) `
              -Function @('Invoke-AdvapiLookupAccountName', 'Invoke-AdvapiLookupAccountSid') `
              -Verbose:$false

enum Carbon_Accounts_Identity_Type
{
    User = 1
    Group
    Domain
    Alias
    WellKnownGroup
    DeletedAccount
    Invalid
    Unknown
    Computer
    Label
}

class Carbon_Accounts_Identity
{
    Carbon_Accounts_Identity([String] $Domain,
                             [String] $Name,
                             [SecurityIdentifier]$Sid,
                             [Carbon_Accounts_Identity_Type]$Type)
    {
        $this.Domain = $Domain;
        $this.Name = $Name;
        $this.Sid = $Sid;
        $this.Type = $Type;

        $this.FullName = $Name
        if ($Domain)
        {
            $this.FullName = "${Domain}\${Name}"
        }
    }

    [String] $Domain

    [String] $FullName

    [String] $Name

    [SecurityIdentifier] $Sid

    [Carbon_Accounts_Identity_Type] $Type

    [bool] Equals([Object] $obj)
    {
        if ($null -eq $obj -or $obj -isnot [Carbon_Accounts_Identity])
        {
            return $false;
        }

        return $this.Sid.Equals($obj.Sid);
    }

    [String] ToString()
    {
        return $this.FullName
    }
}

# Store each of your module's functions in its own file in the Functions
# directory. On the build server, your module's functions will be appended to
# this file, so only dot-source files that exist on the file system. This allows
# developers to work on a module without having to build it first. Grab all the
# functions that are in their own files.
$functionsPath = Join-Path -Path $script:moduleRoot -ChildPath 'Functions\*.ps1'
if( (Test-Path -Path $functionsPath) )
{
    foreach( $functionPath in (Get-Item $functionsPath) )
    {
        . $functionPath.FullName
    }
}



function ConvertTo-CSecurityIdentifier
{
    <#
    .SYNOPSIS
    Converts a string or byte array security identifier into a `System.Security.Principal.SecurityIdentifier` object.
 
    .DESCRIPTION
    `ConvertTo-CSecurityIdentifier` converts a SID in SDDL form (as a string), in binary form (as a byte array) into a
    `System.Security.Principal.SecurityIdentifier` object. It also accepts
    `System.Security.Principal.SecurityIdentifier` objects, and returns them back to you.
 
    If the string or byte array don't represent a SID, an error is written and nothing is returned.
 
    .LINK
    Resolve-CIdentity
 
    .LINK
    Resolve-CIdentityName
 
    .EXAMPLE
    ConvertTo-CSecurityIdentifier -SID 'S-1-5-21-2678556459-1010642102-471947008-1017'
 
    Demonstrates how to convert a a SID in SDDL into a `System.Security.Principal.SecurityIdentifier` object.
 
    .EXAMPLE
    ConvertTo-CSecurityIdentifier -SID (New-Object 'Security.Principal.SecurityIdentifier' 'S-1-5-21-2678556459-1010642102-471947008-1017')
 
    Demonstrates that you can pass a `SecurityIdentifier` object as the value of the SID parameter. The SID you passed
    in will be returned to you unchanged.
 
    .EXAMPLE
    ConvertTo-CSecurityIdentifier -SID $sidBytes
 
    Demonstrates that you can use a byte array that represents a SID as the value of the `SID` parameter.
    #>

    [CmdletBinding()]
    param(
        # The SID to convert to a `System.Security.Principal.SecurityIdentifier`. Accepts a SID in SDDL form as a
        # `string`, a `System.Security.Principal.SecurityIdentifier` object, or a SID in binary form as an array of
        # bytes.
        [Parameter(Mandatory)]
        [Object] $SID
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    try
    {
        if ( $SID -is [string])
        {
            New-Object 'Security.Principal.SecurityIdentifier' $SID
        }
        elseif ($SID -is [byte[]])
        {
            New-Object 'Security.Principal.SecurityIdentifier' $SID,0
        }
        elseif ($SID -is [Security.Principal.SecurityIdentifier])
        {
            $SID
        }
        else
        {
            $msg = "Invalid SID parameter value [$($SID.GetType().FullName)]${SID}. Only " +
                   '[System.Security.Principal.SecurityIdentifier] objects, SIDs in SDDL form as a [String], or SIDs ' +
                   'in binary form as a byte array are allowed.'
            return
        }
    }
    catch
    {
        $sidDisplayMsg = ''
        if ($SID -is [String])
        {
            $sidDisplayMsg = " ""${SID}"""
        }
        elseif ($SID -is [byte[]])
        {
            $sidDisplayMsg = " [$($SID -join ', ')]"
        }
        $msg = "Exception converting SID${sidDisplayMsg} to a [System.Security.Principal.SecurityIdentifier] " +
               'object. This usually means you passed an invalid SID in SDDL form (as a string) or an invalid SID ' +
               "in binary form (as a byte array): ${_}"
        Write-Error $msg -ErrorAction $ErrorActionPreference
        return
    }
}



function Resolve-CIdentity
{
    <#
    .SYNOPSIS
    Gets domain, name, type, and SID information about a user or group.
 
    .DESCRIPTION
    The `Resolve-CIdentity` function takes an identity name or security identifier (SID) and gets its canonical
    representation. It returns a `Carbon_Accounts_Identity` object, which contains the following information about the
    identity:
 
     * Domain - the domain the user was found in
     * FullName - the users full name, e.g. Domain\Name
     * Name - the user's username or the group's name
     * Type - the Sid type.
     * Sid - the account's security identifier as a `System.Security.Principal.SecurityIdentifier` object.
 
    The common name for an account is not always the canonical name used by the operating system. For example, the
    local Administrators group is actually called BUILTIN\Administrators. This function uses the `LookupAccountName`
    and `LookupAccountSid` Windows functions to resolve an account name or security identifier into its domain, name,
    full name, SID, and SID type.
 
    You may pass a `System.Security.Principal.SecurityIdentifer`, a SID in SDDL form (as a string), or a SID in binary
    form (a byte array) as the value to the `SID` parameter. You'll get an error and nothing returned if the SDDL or
    byte array SID are invalid.
 
    If the name or security identifier doesn't represent an actual user or group, an error is written and nothing is
    returned.
 
    .LINK
    Test-CIdentity
 
    .LINK
    Resolve-CIdentityName
 
    .LINK
    http://msdn.microsoft.com/en-us/library/system.security.principal.securityidentifier.aspx
 
    .LINK
    http://msdn.microsoft.com/en-us/library/windows/desktop/aa379601.aspx
 
    .LINK
    ConvertTo-CSecurityIdentifier
 
    .LINK
    Resolve-CIdentityName
 
    .LINK
    Test-CIdentity
 
    .OUTPUTS
    Carbon_Accounts_Identity.
 
    .EXAMPLE
    Resolve-CIdentity -Name 'Administrators'
 
    Returns an object representing the `Administrators` group.
 
    .EXAMPLE
    Resolve-CIdentity -SID 'S-1-5-21-2678556459-1010642102-471947008-1017'
 
    Demonstrates how to use a SID in SDDL form to convert a SID into an identity.
 
    .EXAMPLE
    Resolve-CIdentity -SID ([Security.Principal.SecurityIdentifier]::New()'S-1-5-21-2678556459-1010642102-471947008-1017')
 
    Demonstrates that you can pass a `SecurityIdentifier` object as the value of the SID parameter.
 
    .EXAMPLE
    Resolve-CIdentity -SID $sidBytes
 
    Demonstrates that you can use a byte array that represents a SID as the value of the `SID` parameter.
    #>

    [CmdletBinding()]
    param(
        # The name of the identity to return.
        [Parameter(Mandatory, ParameterSetName='ByName', Position=0)]
        [string] $Name,

        # The SID of the identity to return. Accepts a SID in SDDL form as a `string`, a
        # `System.Security.Principal.SecurityIdentifier` object, or a SID in binary form as an array of bytes.
        [Parameter(Mandatory , ParameterSetName='BySid')]
        [Object] $SID
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    if ($PSCmdlet.ParameterSetName -eq 'BySid')
    {
        $SID = ConvertTo-CSecurityIdentifier -SID $SID
        if (-not $SID)
        {
            return
        }

        $sidBytes = [byte[]]::New($SID.BinaryLength)
        $SID.GetBinaryForm($sidBytes, 0)
        $account = Invoke-AdvapiLookupAccountSid -Sid $sidBytes
        if (-not $account)
        {
            Write-Error -Message "SID ""${SID}"" not found." -ErrorAction $ErrorActionPreference
            return
        }
        return [Carbon_Accounts_Identity]::New($account.ReferencedDomainName, $account.Name, $SID, $account.Use)
    }

    if ($Name.StartsWith('.\'))
    {
        $username = $Name.Substring(2)
        $Name = "$([Environment]::MachineName)\${username}"
        $identity = Resolve-CIdentity -Name $Name
        if (-not $identity)
        {
            $Name = "BUILTIN\${username}"
            $identity = Resolve-CIdentity -Name $Name
        }
        return $identity
    }

    if ($Name.Equals("LocalSystem", [StringComparison]::InvariantCultureIgnoreCase))
    {
        $Name = "NT AUTHORITY\SYSTEM"
    }

    $account = Invoke-AdvapiLookupAccountName -AccountName $Name
    if (-not $account)
    {
        Write-Error -Message "Identity ""${Name}"" not found." -ErrorAction $ErrorActionPreference
        return
    }

    $sid = [SecurityIdentifier]::New($account.Sid, 0)
    $ntAccount = $sid.Translate([NTAccount])
    $domainName,$accountName = $ntAccount.Value.Split('\', 2)
    if (-not $accountName)
    {
        $accountName = $domainName
        $domainName = ''
    }
    return [Carbon_Accounts_Identity]::New($domainName, $accountName, $sid, $account.Use)

}


function Resolve-CIdentityName
{
    <#
    .SYNOPSIS
    Determines the full, NT identity name for a user or group.
 
    .DESCRIPTION
    `Resolve-CIdentityName` resolves a user/group name into its full, canonical name, used by the operating system. For
    example, the local Administrators group is actually called BUILTIN\Administrators. With a canonical username, you
    can unambiguously compare identities on objects that contain user/group information.
 
    If unable to resolve a name into an identity, `Resolve-CIdentityName` returns nothing.
 
    If you want to get full identity information (domain, type, sid, etc.), use `Resolve-CIdentity`.
 
    You can also resolve a SID into its identity name. The `SID` parameter accepts a SID in SDDL form as a `[String]`, a
    `[System.Security.Principal.SecurityIdentifier]` object, or a SID in binary form as an array of bytes. If the SID no
    longer maps to an active account, you'll get the original SID in SDDL form (as a string) returned to you.
 
    .LINK
    ConvertTo-CSecurityIdentifier
 
    .LINK
    Resolve-CIdentity
 
    .LINK
    Test-CIdentity
 
    .LINK
    http://msdn.microsoft.com/en-us/library/system.security.principal.securityidentifier.aspx
 
    .LINK
    http://msdn.microsoft.com/en-us/library/windows/desktop/aa379601.aspx
 
    .OUTPUTS
    string
 
    .EXAMPLE
    Resolve-CIdentityName -Name 'Administrators'
 
    Returns `BUILTIN\Administrators`, the canonical name for the local Administrators group.
    #>

    [CmdletBinding(DefaultParameterSetName='ByName')]
    [OutputType([String])]
    param(
        # The name of the identity to return.
        [Parameter(Mandatory, ParameterSetName='ByName', Position=0)]
        [String] $Name,

        # Get an identity's name from its SID. Accepts a SID in SDDL form as a `string`, a
        # `System.Security.Principal.SecurityIdentifier` object, or a SID in binary form as an array of bytes.
        [Parameter(Mandatory, ParameterSetName='BySid')]
        [Object] $SID
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    if ($PSCmdlet.ParameterSetName -eq 'ByName')
    {
        return Resolve-CIdentity -Name $Name -ErrorAction Ignore | Select-Object -ExpandProperty 'FullName'
    }

    $id = Resolve-CIdentity -Sid $SID -ErrorAction Ignore
    if ($id)
    {
        return $id.FullName
    }

    return $SID.ToString()
}




function Test-CIdentity
{
    <#
    .SYNOPSIS
    Tests that a name is a valid Windows local or domain user/group.
 
    .DESCRIPTION
    Uses the Windows `LookupAccountName` function to find an identity. If it can't be found, returns `$false`.
    Otherwise, it returns `$true`.
 
    Use the `PassThru` switch to return a `[Carbon_Accounts_Identity]` object (instead of `$true` if the identity
    exists).
 
    .LINK
    Resolve-CIdentity
 
    .LINK
    Resolve-CIdentityName
 
    .EXAMPLE
    Test-CIdentity -Name 'Administrators
 
    Tests that a user or group called `Administrators` exists on the local computer.
 
    .EXAMPLE
    Test-CIdentity -Name 'CARBON\Testers'
 
    Tests that a group called `Testers` exists in the `CARBON` domain.
 
    .EXAMPLE
    Test-CIdentity -Name 'Tester' -PassThru
 
    Tests that a user or group named `Tester` exists and returns a `[Carbon_Accounts_Identity]` object if it does.
    #>

    [CmdletBinding()]
    param(
        # The name of the identity to test.
        [Parameter(Mandatory)]
        [string] $Name,

        # Returns a `Carbon.Identity` object if the identity exists.
        [switch] $PassThru
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState

    $identity = Resolve-CIdentity -Name $Name -ErrorAction Ignore
    if (-not $identity)
    {
        return $false
    }

    if ($PassThru)
    {
        return $identity
    }
    return $true
}




function Use-CallerPreference
{
    <#
    .SYNOPSIS
    Sets the PowerShell preference variables in a module's function based on the callers preferences.
 
    .DESCRIPTION
    Script module functions do not automatically inherit their caller's variables, including preferences set by common
    parameters. This means if you call a script with switches like `-Verbose` or `-WhatIf`, those that parameter don't
    get passed into any function that belongs to a module.
 
    When used in a module function, `Use-CallerPreference` will grab the value of these common parameters used by the
    function's caller:
 
     * ErrorAction
     * Debug
     * Confirm
     * InformationAction
     * Verbose
     * WarningAction
     * WhatIf
     
    This function should be used in a module's function to grab the caller's preference variables so the caller doesn't
    have to explicitly pass common parameters to the module function.
 
    This function is adapted from the [`Get-CallerPreference` function written by David Wyatt](https://gallery.technet.microsoft.com/scriptcenter/Inherit-Preference-82343b9d).
 
    There is currently a [bug in PowerShell](https://connect.microsoft.com/PowerShell/Feedback/Details/763621) that
    causes an error when `ErrorAction` is implicitly set to `Ignore`. If you use this function, you'll need to add
    explicit `-ErrorAction $ErrorActionPreference` to every `Write-Error` call. Please vote up this issue so it can get
    fixed.
 
    .LINK
    about_Preference_Variables
 
    .LINK
    about_CommonParameters
 
    .LINK
    https://gallery.technet.microsoft.com/scriptcenter/Inherit-Preference-82343b9d
 
    .LINK
    http://powershell.org/wp/2014/01/13/getting-your-script-module-functions-to-inherit-preference-variables-from-the-caller/
 
    .EXAMPLE
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
 
    Demonstrates how to set the caller's common parameter preference variables in a module function.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        #[Management.Automation.PSScriptCmdlet]
        # The module function's `$PSCmdlet` object. Requires the function be decorated with the `[CmdletBinding()]`
        # attribute.
        $Cmdlet,

        [Parameter(Mandatory)]
        # The module function's `$ExecutionContext.SessionState` object. Requires the function be decorated with the
        # `[CmdletBinding()]` attribute.
        #
        # Used to set variables in its callers' scope, even if that caller is in a different script module.
        [Management.Automation.SessionState]$SessionState
    )

    Set-StrictMode -Version 'Latest'

    # List of preference variables taken from the about_Preference_Variables and their common parameter name (taken
    # from about_CommonParameters).
    $commonPreferences = @{
                              'ErrorActionPreference' = 'ErrorAction';
                              'DebugPreference' = 'Debug';
                              'ConfirmPreference' = 'Confirm';
                              'InformationPreference' = 'InformationAction';
                              'VerbosePreference' = 'Verbose';
                              'WarningPreference' = 'WarningAction';
                              'WhatIfPreference' = 'WhatIf';
                          }

    foreach( $prefName in $commonPreferences.Keys )
    {
        $parameterName = $commonPreferences[$prefName]

        # Don't do anything if the parameter was passed in.
        if( $Cmdlet.MyInvocation.BoundParameters.ContainsKey($parameterName) )
        {
            continue
        }

        $variable = $Cmdlet.SessionState.PSVariable.Get($prefName)
        # Don't do anything if caller didn't use a common parameter.
        if( -not $variable )
        {
            continue
        }

        if( $SessionState -eq $ExecutionContext.SessionState )
        {
            Set-Variable -Scope 1 -Name $variable.Name -Value $variable.Value -Force -Confirm:$false -WhatIf:$false
        }
        else
        {
            $SessionState.PSVariable.Set($variable.Name, $variable.Value)
        }
    }
}