Badgerati / Pode

Pode is a Cross-Platform PowerShell web framework for creating REST APIs, Web Sites, and TCP/SMTP servers
https://badgerati.github.io/Pode
MIT License
856 stars 95 forks source link

Using ActiveDirectory module with Pwsh 7.1.3 and IIS #739

Closed jhainau closed 3 years ago

jhainau commented 3 years ago

Question

I'm putting this as a question, because I'm sure I'm just not doing it right... I am using the latest Pode module (2.2.3), powershell core 7.1.3, dotnet-hosting-5.0.6-win When I have the line "Import-module ActiveDirectory" or Import-podemodule activedirectory, it fails in iis, but not when run directly in powershell. both display the warning:

WARNING: Module activedirectory is loaded in Windows PowerShell using WinPSCompatSession remoting session; please note that all input and output of commands from this module will be deserialized objects. If you want to load this module into PowerShell please use 'Import-Module -SkipEditionCheck' syntax..

but IIS has the following in the stdout log: using import-module activedirectory

WARNING: Error initializing default drive: 'Unable to contact the server. This may be because this server does not exist, it is currently down, or it does not have the Active Directory Web Services running.'. WARNING: Error initializing default drive: 'Unable to contact the server. This may be because this server does not exist, it is currently down, or it does not have the Active Directory Web Services running.'. WARNING: Module activeDirectory is loaded in Windows PowerShell using WinPSCompatSession remoting session; please note that all input and output of commands from this module will be deserialized objects. If you want to load this module into PowerShell please use 'Import-Module -SkipEditionCheck' syntax. ParentContainsErrorRecordException: C:\Program Files\WindowsPowerShell\Modules\Pode\2.2.3\Private\Server.ps1:114 Line |  114 |  throw $.Exception  |  ~~~~~~  | Failed to import module: ActiveDirectory  WARNING: Error initializing default drive: 'Unable to contact the server. This may be because this server does not exist, it is currently down, or it does not have the Active Directory Web Services running.'. WARNING: Error initializing default drive: 'Unable to contact the server. This may be because this server does not exist, it is currently down, or it does not have the Active Directory Web Services running.'. WARNING: Module activeDirectory is loaded in Windows PowerShell using WinPSCompatSession remoting session; please note that all input and output of commands from this module will be deserialized objects. If you want to load this module into PowerShell please use 'Import-Module -SkipEditionCheck' syntax. ParentContainsErrorRecordException: C:\Program Files\WindowsPowerShell\Modules\Pode\2.2.3\Private\Server.ps1:114 Line |  114 |  throw $.Exception  |  ~~~~~~  | Failed to import module: ActiveDirectory 

IIS stdoutlog using import-podemodule activedirectory

Exception: C:\Program Files\WindowsPowerShell\Modules\Pode\2.2.3\Public\Utilities.ps1:471 Line |  471 |  throw "Failed to import module: $(Protect-PodeValue -Value $P .  |  ~~~~~~~~~~~~~  | Failed to import module: activeDirectory  Exception: C:\Program Files\WindowsPowerShell\Modules\Pode\2.2.3\Public\Utilities.ps1:471 Line |  471 |  throw "Failed to import module: $(Protect-PodeValue -Value $P .  |  ~~~~~~~~~~~~~  | Failed to import module: activeDirectory 

Badgerati commented 3 years ago

Hey @jhainau,

I just tried this myself, using the same versions, but for me Import-Module ActiveDirectory works under IIS each time with just the warning:

WARNING: Module ActiveDirectory is loaded in Windows PowerShell using WinPSCompatSession remoting session; please note
that all input and output of commands from this module will be deserialized objects. If you want to load this module into
PowerShell please use 'Import-Module -SkipEditionCheck' syntax.

I've got IIS running, the software version you mentioned, and RSAT AD DS/LDS Tools installed.

Would you be able to give a snippet of the script where you import AD, and of your web.config?

jhainau commented 3 years ago

Server.ps1:


Import-Module -Name Pode
Import-Module -Name Pode.Web
import-Module -Name activeDirectory
#import-Module -Name PCPerson #installed from custom gallery

[System.Net.ServicePointManager]::SecurityProtocol = $([System.Net.ServicePointManager]::SecurityProtocol), [System.Net.SecurityProtocolType]::Tls12

Start-PodeServer {

Add-PodeEndpoint -Address * -Port 80 -Protocol Http 
    Import-PodeModule -name ActiveDirectory
    $BaseURL = "$([System.Net.Dns]::GetHostEntry($Env:ComputerName).HostName)"
Set-PodeState -Name BaseURL -Value $BaseURL | Out-Null

 New-PodeAuthScheme -Basic | Add-PodeAuth -Name 'Login' -Sessionless -ScriptBlock {
        param($username, $password)

        <# # here you'd check a real user storage, this is just for example
        if ($username -eq 'morty' -and $password -eq 'pickle') {
            return @{
                User = @{
                    'ID' ='M0R7Y302'
                    'Name' = 'Morty';
                    'Type' = 'Human';
                }
            }
        }
 #>

    $SecPassword = ConvertTo-SecureString -String $password -AsPlainText -Force
    $userCred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $username, $SecPassword

    $ADObject = $(try { Get-ADUser -Properties memberof,SID,SamAccountName,UserPrincipalName -Identity $username -Credential $userCred -ErrorAction Stop }Catch { $false })

    if ($ADObject -ne $false) {
        # success
        # here you store some data that only gets passed server side and never over the wire
        #* I'm using this as an area to store authorizations for
        #* services that the user has permissions to...
        # Person is the name of a HR system we use. it has it's own module
        #$personBasics = Get-PersonLookup -Source person -loginName $username
       # $assignedSystems = Get-PersonUserSystem -Identity $username -Source person -listall | Where-Object { [string]::IsNullOrEmpty($_.stopDt) }
        #$assignedRoles = Get-PersonAssignedRole -TargetUser $username -Source person | Where-Object { [string]::IsNullOrEmpty( $_.endDt) }
        return @{ User = [ordered]@{
                'ID'       = $ADObject.SID
                'Name'     = $ADObject.SamAccountName
                'UPN'      = $ADObject.UserPrincipalName
                'AdGroups' = $ADObject.memberof
                'cred'     = $userCred
                #"Systems"  = $assignedSystems.system
                #"roles"    = $assignedRoles
                #'userInfo' = $personBasics
            }
        }
    }
        # authentication failed
        return $null
    }
Add-PodeRoute -Method Get -path '/UserInfo' -Authentication 'Login' -ScriptBlock {
    #param ($e)
    $result = $WebEvent.auth.user
    $result.remove('cred')
    $html = @"
<!DOCTYPE HTML>
<html>

<head>
<title>
Custom API Service: UserInfo
</title>

<body style="text-align:center;" id="body">

<h1 style="color:green;">
User Information
</h1>

<p id="GFG_UP" style="font-size: 15px; font-weight: bold;">
</p>
<p align="left">
<pre align="left">
$($result |ConvertTo-Json -Depth 5)
</pre>
</p>
<br><br>
<!-- other stuff could go here-->

</body>

</html>
"@
    Write-PodeHtmlResponse -Value $html
}

# GET request for web page on "localhost/"
Add-PodeRoute -Method Get -Path '/' -ScriptBlock {
    Write-PodeTextResponse -value 'hello world'
}
}

Web.config:


<configuration>
  <location path="." inheritInChildApplications="false">
    <system.webServer>
      <handlers accessPolicy="Read, Execute, Script">
        <remove name="WebDAV" />
        <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
        <remove name="ExtensionlessUrlHandler-Integrated-4.0" />
        <add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="*" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
        <remove name="ExtensionlessUrl-Integrated-4.0" />
        <add name="ExtensionlessUrl-Integrated-4.0" path="*." verb="*" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
      </handlers>

      <modules>
        <remove name="WebDAVModule" />
      </modules>

      <aspNetCore processPath="pwsh.exe" arguments=".\server.ps1" stdoutLogEnabled="true" stdoutLogFile=".\logs\stdout" hostingModel="OutOfProcess" />

      <security>
        <authorization>
          <remove users="*" roles="" verbs="" />
          <add accessType="Allow" users="*" verbs="GET,HEAD,POST,PUT,DELETE,DEBUG,OPTIONS" />
        </authorization>
      </security>
    </system.webServer>
  </location>
</configuration>
Badgerati commented 3 years ago

Hey @jhainau,

Thanks!

Import-PodeModule not working makes sense here, as the way it works it'll be trying to find the ActiveDirectory module under /PowerShell not /WindowsPowerShell , thus failing. Just Import-Module like you have at the top is the better way to go 😃

With that, and removing the Import-PodeModule, the site starts in IIS without error.

Now, if I try to hit /UserInfo, I start getting a different error:

Creating a new session for implicit remoting of "Get-ADUser" command...

But I was able to get around this by change the Get-ADUser call to the following, and by moving the ActiveDirectory import as well:

$ADObject = Invoke-Command -ScriptBlock {
    param($username)
    Import-Module -Name ActiveDirectory
    try {
        Get-ADUser -Properties memberof,SID,SamAccountName,UserPrincipalName -Identity $username -ErrorAction Stop
    }
    catch {
        $false
    }
} -ArgumentList $username

Then /UserInfo worked and returned the HTML with user information.

This was the full script I had in the end:

Import-Module -Name Pode

[System.Net.ServicePointManager]::SecurityProtocol = $([System.Net.ServicePointManager]::SecurityProtocol), [System.Net.SecurityProtocolType]::Tls12

Start-PodeServer {
    Add-PodeEndpoint -Address * -Port 80 -Protocol Http
    $BaseURL = "$([System.Net.Dns]::GetHostEntry($Env:ComputerName).HostName)"
    Set-PodeState -Name BaseURL -Value $BaseURL | Out-Null

    New-PodeAuthScheme -Basic | Add-PodeAuth -Name 'Login' -Sessionless -ScriptBlock {
        param($username, $password)

        $SecPassword = ConvertTo-SecureString -String $password -AsPlainText -Force
        $userCred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $username, $SecPassword

        $ADObject = Invoke-Command -ScriptBlock {
            param($username)
            Import-Module -Name ActiveDirectory
            try {
                Get-ADUser -Properties memberof,SID,SamAccountName,UserPrincipalName -Identity $username -ErrorAction Stop
            }
            catch {
                $false
            }
        } -ArgumentList $username

        if ($ADObject -ne $false) {
            return @{ User = [ordered]@{
                    'ID'       = $ADObject.SID
                    'Name'     = $ADObject.SamAccountName
                    'UPN'      = $ADObject.UserPrincipalName
                    'AdGroups' = $ADObject.memberof
                    'cred'     = $userCred
                }
            }
        }

        # authentication failed
        return $null
    }

    Add-PodeRoute -Method Get -path '/UserInfo' -Authentication 'Login' -ScriptBlock {
        $result = $WebEvent.auth.user
        $result.remove('cred')
        $html = @"
<!DOCTYPE HTML>
<html>

<head>
<title>
Custom API Service: UserInfo
</title>

<body style="text-align:center;" id="body">

<h1 style="color:green;">
User Information
</h1>

<p id="GFG_UP" style="font-size: 15px; font-weight: bold;">
</p>
<p align="left">
<pre align="left">
$($result |ConvertTo-Json -Depth 5)
</pre>
</p>
<br><br>
</body>
</html>
"@
        Write-PodeHtmlResponse -Value $html
    }

    # GET request for web page on "localhost/"
    Add-PodeRoute -Method Get -Path '/' -ScriptBlock {
        Write-PodeTextResponse -value 'hello world'
    }
}

Hope that helps! :)

jhainau commented 3 years ago

I followed your instructions, but get the same results, unable to load the Active directory module from IIS... but works outside of iis in both pwsh ad Windows PowerShell I suspect I am not setting up the IIS portion correctly as the instructions were ... less than thorough... I am not well versed with IIS... I may just need to set my instance up as a service using NSSM... but it is also a little lacking in thorough instructions... not to mention, NSSM has not been updated in a few years... but seams to be generally well liked and trusted. sorry for my lack of knowledge in these areas. on a side note... the reason I was using the "-credential" in the get-aduser bit was because I assume that if you have a valid ad account and password, it should be able to return some info the associated ad account... that was how I was performing the authentication... if the ad and password combo failed to return the info for the requesting account, then I assumed that the password or userID was incorrect

Badgerati commented 3 years ago

There isn't anything special I really do with IIS when setting up the site; I just install the software from the docs page, create a basic website and binding, and point it to the directory my script is in. When you say it's lacking, is there some extra steps you had to do in order to get IIS to work? Something we could add to the docs, or is it flesh out the part where it says "Create a site in IIS and add a binding"?

For NSSM, it's there because it is just commonly used to run things like PowerShell as a Windows Service. You don't have to use NSSM, if you've a preferred way of doing it using another tool then that's fine - maybe we could even document it here. Even a Scheduled Task is fine 😛

Loading the module in a main pwsh terminal is different to running it in IIS, when in IIS it is running in pwsh, but in an almost background remote session. A quick Google of the error AD gives when loading suggests other things that could be causing it to fail:

I would says it more an issue outside of IIS, preventing it from loading; as if you take the AD module/logic out and your server loads fine, then that's IIS done and working.

As for -Credential missing that was just an artifact from my end, as I was originally testing with a PSSession. You can just add the $userCreds to the Invoke-Command -ArgumentList; I just forgot to put it back in.

RobinBeismann commented 3 years ago

Just curious, are you running the IIS App Pool using a local (non-domain) account maybe?

jhainau commented 3 years ago

Yes, to the first question... Fleshing out the "Create a site in IIS and add binding" would be immensely helpful... so sorry, as for NSSM... this does seem to be the Service Manager of choice - even in my organization... I was just surprised that such a popular tool would be years since the last update. as for IIS App Pool with local account... Maybe? My day job is Desktop Support. I learned PowerShell out of necessity to automate the mind numbing repetitive tasks I perform. I only just started looking into IIS and NSSM in support of my project that uses Pode.
You have been very patient and understanding, Thank you :-)

Badgerati commented 3 years ago

I'll look at getting that part fleshed out then! 😃

What @RobinBeismann said is likely the cause; if you followed the docs and let the site create the app-pool, and changed nothing else, the app-pool could be running as the ApplicationPoolIdentiy user. Changing that to a domain user should hopefully fix it.

I do it out of habit so much, I didn't even think about it, oops!

jhainau commented 3 years ago

Got it! I had the IIS settings not quite right... I forgot to Exclude the logs folder so that it didn't restart the server on every request, I followed the advice from you and @RobinBeismann with regards to starting the app pool with domain credentials, and the last thing was in IIS Authentication for my site, edit Basic Authentication and enter my domain in the Default domain. everything is working as expected... for now Thank you so much!

bjerrecs commented 4 months ago

Is there any chance to get the -UseUPN flag? I have a scenario where i have 100+ different @doamin