Quick Learn – Specify Which Functions Can Invoke Another Function

I have a function called Send-ErrorMessage. Its sole purpose is to send me an email if the catch block of a try-catch statement executes. Now, not just any catch block, as there can be more than one, but one where a specific error exception is not included.

The below example obtains a random Exchange server name as part of its try block. If that command produces an error, one of two catch blocks will execute. The first catch block will execute if the error produces a Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException exception. The exception is listed in square brackets after the catch keyword. The second catch block will execute if it’s any other exception. This, is the error I want to know about. When the code in the second catch block executes, it first creates a parameter hash table and it then sends that to the Send-ErrorMessage function, where the error is presented to the user, and an email is sent.

try {
    # Randomly select Exchange Server.
    $ExchServ = Get-Random (Get-ADGroupMember $ADGroup |
       Where-Object -FilterScript {$_.objectClass -eq 'computer'}).Name

} catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] {
    Write-Warning -Message "Cannot find object with the identity $ADGroup."

} catch {
    # Unknown error exception.
    $ErrorParams = @{
        FunctionName = $MyInvocation.MyCommand.Name
        ErrorMessage = $Error[0].ToString()
        ErrorType = $Error[0].Exception.GetType().FullName
    }
    Send-ErrorMessage @ErrorParams
}

At first, I decided that the Send-ErrorMessage function wasn’t to be exported from its module. This meant that only the other functions in that module, would be able to use Send-ErrorMessage. This was done to prevent a human from directly interacting with the Send-ErrorMessage function. In time, I had a decision to make, as I decided I wanted to invoke Send-ErrorMessage from functions that weren’t in the same module, where Send-ErrorMessage was declared.

To do this, I had to export Send-ErrorMessage, thus making it available to all my functions regardless of module, and anyone that invoked the function manually. It was this second part I wanted to prevent; I didn’t want a user to be able to use this function. I only wanted specific functions, in varying modules, to be allowed to use the Send-ErrorMessage function.

What I needed was a way to prevent the function from doing much, if it was invoked by something other than an approved function. To do this required me to know what function invoked the Send-ErrorMessage function while the Send-ErrorMessage function was currently being invoked. I hadn’t considered this before, and so it took some thinking and testing. In the end, I have a solution.

To help demonstrate, let’s start with two functions: The first one is allowed to invoke Send-ErrorMessage, and the second one is not allowed to invoke Send-ErrorMessage. You’ll see this in the upcoming Send-ErrorMessage function. Before we get to that function, though, get comfortable with the two below functions.

Function Test-Allowed {
    # Divide by 0 to force error/catch block.
    try {
        10 / 0
    } catch {
        Send-ErrorMessage
    }
} # End Function: Test-Allowed.


Function Test-NotAllowed {
    # Divide by 0 to force error/catch block.
    try {
        10 / 0
    } catch {
        Send-ErrorMessage
    }
} # End Function: Test-NotAllowed.

They work this way: try to divide by 0 and intentionally error. The catch block will then invoke Send-ErrorMessage. Inside Send-ErrorMessage is where the determination is made as to whether or not the function that invoked it, is allowed to continue to use it. If the function that invoked it, is in the $AcceptableFunctions variable, then the code to send an email with the error information, which isn’t actually included in the below function, can be sent. If the function isn’t in the $AcceptableFunctions variable, then the user will get a warning and nothing will be sent.

Function Send-ErrorMessage {
    [CmdletBinding()]
    Param (
    )

    Begin {
        # This variable can be set a number of ways to include Get-Command.
        $AcceptableFunctions = 'Get-AJob','Test-Allowed','Get-Real'
        $CallingFunction = (Get-PSCallStack)[1].Command
        
        $Proceed = $false
        If ($CallingFunction -in $AcceptableFunctions) {
            $Proceed = $true
        }
    } # End Begin.

    Process {
        If ($Proceed) {
            Write-Verbose -Message "The calling function ($CallingFunction) is approved." -Verbose
            Write-Verbose -Message 'Beginning to send error message ...' -Verbose
            # DO STUFF!!!
        } Else {
            Write-Warning -Message "The calling function ($CallingFunction) is NOT approved."
            Write-Warning -Message "This function ($($MyInvocation.MyCommand.Name)) can only be invoked by approved functions."
        }
    } # End Process.
    
    End {
    } # End End.
} # End Function: Send-ErrorMessage.

Let’s invoke Send-ErrorMessage now, by invoking both the Test-Allowed and Test-NotAllowed functions. In the below example, the first two lines, that start with VERBOSE, are returned from invoking the Test-Allowed function, and the last two lines, that start with WARNING, are returned from invoking the Test-NotAllowed function. It works just like it should, allowing me to do something (send that error email), when I trust the calling function.

# Invoke Function.
Test-Allowed

# Invoke Function.
Test-NotAllowed

VERBOSE: The calling function (Test-Allowed) is approved.
VERBOSE: Beginning to send error message ...
WARNING: The calling function (Test-NotAllowed) is NOT approved.
WARNING: This function (Send-ErrorMessage) can only be invoked by approved functions.

Here’s the entire code, so you can copy it to the ISE, etc. and test it for yourself.

Function Send-ErrorMessage {
    [CmdletBinding()]
    Param (
    )

    Begin {
        # This variable can be set a number of ways to include Get-Command.
        $AcceptableFunctions = 'Get-AJob','Test-Allowed','Get-Real'
        $CallingFunction = (Get-PSCallStack)[1].Command
        
        $Proceed = $false
        If ($CallingFunction -in $AcceptableFunctions) {
            $Proceed = $true
        }
    } # End Begin.

    Process {
        If ($Proceed) {
            Write-Verbose -Message "The calling function ($CallingFunction) is approved." -Verbose
            Write-Verbose -Message 'Beginning to send error message ...' -Verbose
            # DO STUFF!!!
        } Else {
            Write-Warning -Message "The calling function ($CallingFunction) is NOT approved."
            Write-Warning -Message "This function ($($MyInvocation.MyCommand.Name)) can only be invoked by approved functions."
        }
    } # End Process.
    
    End {
    } # End End.
} # End Function: Send-ErrorMessage.


Function Test-Allowed {
    # Divide by 0 to force error/catch block.
    try {
        10 / 0
    } catch {
        Send-ErrorMessage
    }
} # End Function: Test-Allowed.


Function Test-NotAllowed {
    # Divide by 0 to force error/catch block.
    try {
        10 / 0
    } catch {
        Send-ErrorMessage
    }
} # End Function: Test-NotAllowed.


# Invoke Function.
Test-Allowed

# Invoke Function.
Test-NotAllowed

My first thought after these functions were written and tested, was what happens now that Send-ErrorMessage is in memory. Can I, as a user, just invoke Send-ErrorMessage myself? I can try, but I won’t get anywhere, as is demonstrated below.

Send-ErrorMessage
WARNING: The calling function (<ScriptBlock>) is NOT approved.
WARNING: This function (Send-ErrorMessage) can only be invoked by approved functions.

Keep in mind other ways to assign the $AcceptedFunctions variable. This doesn’t, and realistically, shouldn’t need to be a static. Instead, use the Get-Command cmdlet to collect the functions that will be allowed to invoke Send-ErrorMessage. If you use a prefix in your function names, it’s easy to isolate which functions can invoke Send-ErrorMessage. The below example assumes you prefix the nouns in your functions with Work, such as Get-WorkUser, and Get-WorkJob.

$AcceptableFunctions = (Get-Command -Noun Work*).Name

In closing, if you didn’t pick it up yourself, this works in thanks to the Get-PSCallStack function. It stores the called commands in a reverse array. That means, that the final function, Send-ErrorMessage, in this example, is index 0: (Get-PSCallStack)[0]. Therefore, the calling function, or command run in the console, is going to be (Get-PSCallStack)[1]. Tada! And to think, just a day or two ago, and I don’t think I had ever used the Get-PSCallStack cmdlet.

Update: There’s a Part II: Read it here.

Leave a Reply

Your email address will not be published.