Desktop and Application Streaming

Active Directory Group Membership Based AppStream 2.0 Application Targeting

The default behavior of an Amazon AppStream 2.0 Stack is to present all the applications to the end user that were added to the application catalog by the administrator creating the image. Customers accustomed to targeting individual applications to end users based on Active Directory group membership can also continue to do so using the dynamic application framework feature of AppStream 2.0. This flexibility allows the customer to maintain fewer images by adding additional applications to an AppStream 2.0 fleet that not all of their user base are users of. Only revealing those applications to the end users which have been granted access via an application-specific group.

This blog details how to get started using the dynamic application framework and PowerShell to provide Active Directory group membership-based application targeting in AppStream 2.0.

Prerequisites

This tutorial assumes that you have the following installed and configured:

Walkthrough

This article shows you how to perform the following tasks:

  1. Enable the dynamic app provider on an AppStream 2.0 Image Builder.
  2. Implement the PowerShell script and create the associated Windows scheduled task that drives the solution.
  3. Create a CSV file containing the required application information.
  4. Optionally: Configure the solution to host the CSV file in Amazon S3 instead of locally within the image.
  5. Optionally: Configure the solution to use an Amazon DynamoDB table for storing application information instead of the local CSV file.

Enable the Dynamic Application Provider

Before configuring the Image Builder, compile the required dynamic application framework and Apache Thrift dynamic link libraries (DLLs). For more information, please refer to the Create a PowerShell-Based dynamic app provider in Amazon AppStream 2.0 blog post. Update: Due to C# changes in Thrift from version 0.13.0, use version 0.12.0 or earlier.

  1. Create a directory where the dynamic app framework and complimentary files will reside. We will be using C:\AS2DAF for the rest of this guide. If you choose to use a different location, ensure that all references consistently use this alternate location as you follow the steps outlined below.
  2. Copy the AS2DAF.dll and Thrift.dll files created during the prerequisite step into the C:\AS2DAF directory.
  3. Open the Registry Editor on the Image Builder instance, and create the following registry key:

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\AS2DAF]
"DisplayName"="AS2DAF"

This is the registry key that the AppStream 2.0 dynamic app framework will check for in order to enable the feature.

  1. Navigate to C:\ProgramData\Amazon\AppStream\AppCatalogHelper\DynamicAppCatalog\, and open the Agents.json configuration file in a text editor.
  2. Replace DisplayName with the value created in the registry in the previous step: AS2DAF.
  3. Replace Path with the directory created in step 1: C:\AS2DAF. Note that any slashes in the path must be doubled-up.
  4. The updated Agents.json file should look similar to the following:
    {
       "Agents":[
          {
             "DisplayName":"AS2DAF",
             "Path":"C:\\AS2DAF"
          }
       ]
    }
  5. Place the script file, AS2_DAF_Login.ps1, in the C:\AS2DAF directory created in the previous section, alongside the DLL files. Be sure to update the paths to the DLL files inside the script if they were placed somewhere other than C:\AS2DAF.
    #############################################################################
    ## Variables
    #############################################################################
    Add-Type -Path "C:\AS2DAF\AS2DAF.dll"                 #Define the full path to the DAF DLL
    Add-Type -Path "C:\AS2DAF\Thrift.dll"                 #Define the full path to the Thrift DLL
    
    $ADGroupPrefix = "App Grant*"                         #Modify to match AD Group Prefix for AS2 application assignments in your Active Directory
    $AppCatalogMethod = "local"                              #Set to method for application catalog (dynamodb, s3, local)
    
    #Edit if using "local" method
    $AppCatalogLocalCsvPath = "c:\AS2DAF\apps.csv"        #Define the path to the CSV file containing installed applications, use with "local" method
    
    #Edit if using "dynamoDB" method
    $AppCatalogDynamoDBName = "as2_daf_appcatalog"        #DynamoDB Table Name containing installed application information, use with "dynamodb" method
    
    #Edit if using "S3" method
    $AppCatalogS3Bucket     = "as2-daf"                   #S3 bucket name containing install application CSV file, use with "s3" method
    $AppCatalogS3FileName   = "s3_apps.csv"               #Name of the CSV file containing installed applications stored in S3, use with "s3" method" 
    
    $AppCatalogRegion = (Invoke-WebRequest -UseBasicParsing -Uri http://169.254.169.254/latest/dynamic/instance-identity/document | ConvertFrom-Json | Select region).region
    
    #Logging Function
    Function Write-Log {
        Param ([string]$message)
        $stamp = (Get-Date).toString("yyyy/MM/dd HH:mm:ss")
        $logoutput = "$stamp $message"
        Add-content $logfile -value $logoutput
    }
    
    #Establish a connection to the Thirft server, exposed via Named Pipes
    Function Connect-Thrift {
        Write-Log "Opening connection to AppStream 2.0 Thrift server."
        $script:transport = New-Object -TypeName Thrift.Transport.TNamedPipeClientTransport('D56C0258-2173-48D5-B0E6-1EC85AC67893')
        $script:protocol = New-Object -TypeName Thrift.Protocol.TBinaryProtocol($transport)
        $script:thriftclient = New-Object -TypeName AppStream.ApplicationCatalogService.Model.ApplicationCatalogService+Client($protocol)
        $transport.open()
        Write-Log "Connection successful."
    }
    
    #Close the connection to the Thrift server
    Function Close-Thrift {
        Write-Log "Application publishing complete."
        Write-Log "Closing connection to AppStream 2.0 Thrift server."
        $transport.close()
    }
    
    #Create Log file for logged in user
    $currentuser = (get-wmiobject Win32_ComputerSystem).UserName.Split('\')[1]
    
    $logfile = "C:\Users\$currentuser\Documents\Logs\DAFClient\$currentuser-$(get-date -f MM-dd-yyyy_HH_mm_ss)-dafupdate.log"
    New-Item -path $logfile -ItemType File -Force | Out-Null
    
    #Get the current user's SID
    $usersid = (New-Object System.Security.Principal.NTAccount(($currentuser))).Translate([System.Security.Principal.SecurityIdentifier]).value
    
    #Get AD group membership for logged in user
    Write-Log "Gathering user's ($currentuser) AD group membership."
    $usergroups = ([ADSISEARCHER]"samaccountname=$($currentuser)").Findone().Properties.memberof -replace '^CN=([^,]+).+$','$1' | Where-Object {$_  -Like $ADGroupPrefix}
    
    switch ($AppCatalogMethod) {
        "local"  {
            Write-Log "Using Local CSV AppCatalog method."
            #If the CSV does not exist, exit the script
            if (!(Test-Path -Path $AppCatalogLocalCsvPath)) {
                Write-Log "No application catalog file found. Exiting."
                Exit
            }
          
            Connect-Thrift
    
            Write-Log "Generating approved application list for $currentuser (SID: $usersid)"
    
            #Get the content of the CSV file containing details of installed applications
            foreach ($app in (Import-Csv -path $AppCatalogLocalCsvPath)) {
    
                #Grab the AD Group for current application to compare against user's AD group membership
                $appGroup = $app.ADGroup
                if ($usergroups.Contains($appGroup)) {
                    write-Log "User is a member of $appGroup"
        
                    $appId = $app.Id
                    $appname = $app.DisplayName
                    $apppath = $app.LaunchPath
                    $appicon = $app.IconData
    
                    Write-Log "Adding $appname to the dynamic application catalog..."
    
                    #Because user is a member of the group, add the application to the AppStream catalog dynamically
                    $applist = New-Object -TypeName AppStream.ApplicationCatalogService.Model.Application("$appId", "$appname", "$apppath", "$appicon")
                    $getappreq = New-Object -TypeName Appstream.ApplicationCatalogService.Model.AddApplicationsRequest($usersid, $applist) 
                    $addapp = $thriftclient.AddApplications($getappreq)
                }
            }
    
            Close-Thrift
            break
        }
        "s3"   {
            Write-Log "Using S3 hosted CSV AppCatalog method."
            $tmp = $env:TEMP
    
            try {
                Write-Log "Downloading CSV file ($AppCatalogS3FileName) from S3 bucket ($AppCatalogS3Bucket) using instance profile role."
                $result = Read-S3Object -BucketName $AppCatalogS3Bucket -Key $AppCatalogS3FileName -File "$tmp\local-apps.csv" -ProfileName appstream_machine_role
                Write-Log "Copied CSV file ($AppCatalogS3FileName) from S3 bucket ($AppCatalogS3Bucket) to $tmp\local-apps.csv"
            }
            catch {
                Write-Log "Error downloading CSV file ($AppCatalogS3FileName) from S3 bucket ($AppCatalogS3Bucket). Exiting"
                Exit
            }
    
            Connect-Thrift
    
            Write-Log "Generating approved application list for $currentuser (SID: $usersid)"
    
            #Get the content of the CSV file containing details of installed applications
            foreach ($app in (Import-Csv -path "$tmp\local-apps.csv")) {
    
                #Grab the AD Group for current application to compare against user's AD group membership
                $appGroup = $app.ADGroup
                if ($usergroups.Contains($appGroup)) {
                    write-Log "User is a member of $appGroup"
        
                    $appId = $app.Id
                    $appname = $app.DisplayName
                    $apppath = $app.LaunchPath
                    $appicon = $app.IconData
    
                    Write-Log "Adding $appname to the dynamic application catalog..."
    
                    #Because user is a member of the group, add the application to the AppStream catalog dynamically
                    $applist = New-Object -TypeName AppStream.ApplicationCatalogService.Model.Application("$appId", "$appname", "$apppath", "$appicon")
                    $getappreq = New-Object -TypeName Appstream.ApplicationCatalogService.Model.AddApplicationsRequest($usersid, $applist) 
                    $addapp = $thriftclient.AddApplications($getappreq)
                }
            }
    
            Close-Thrift
    
            #Delete temporary CSV File
            Write-Log "Removing temorary CSV file, $tmp\local-apps.csv"
            $result = Remove-Item -Path "$tmp\local-apps.csv" -Force
    
            break
        }
        "dynamodb" {
            Write-Log "Using DynamoDB AppCatalog method."
            try {
                
                #Grab credential token from instance profile
                $creds = Get-AWSCredential -ProfileName appstream_machine_role
                Write-Log "Obtained token credentials from instance role."
    
                #Connect to DynamoDB table and scan for all application entries
                Write-Log "Connecting to DynamoDB table ($AppCatalogDynamoDBName)."
                $regionEndpoint=[Amazon.RegionEndPoint]::GetBySystemName($AppCatalogRegion)
                $ddbclient = New-Object Amazon.DynamoDBv2.AmazonDynamoDBClient($creds, $regionEndpoint)
                $request = New-Object Amazon.DynamoDBv2.Model.ScanRequest
                $request.TableName = $AppCatalogDynamoDBName
                $search = $ddbclient.Scan($request)
                Write-Log "Successfully scanned DynamoDB table ($AppCatalogDynamoDBName)."
    
                #Convert DynamoDB results to PowerShell object
                $result = $search.Items     
                $result_json = $result  | ConvertTo-JSON
                $result_powershell = $result_json | ConvertFrom-Json
    
            }
            catch {
                Write-Log "Error adding from DynamoDB table ($AppCatalogDynamoDBName). Exiting"
                Exit
            }
     
            Connect-Thrift
    
            Write-Log "Generating approved application list for $currentuser (SID: $usersid)"
    
            foreach ($item in $result_powershell) {
                $appGroup = $item.ADGroup.S
    
                if ($usergroups.Contains($appGroup)) {
                    write-Log "User is a member of $appGroup"
    
                    $apppath = $item.LaunchPath.S                
                    $appname = $item.DisplayName.S
                    $appworkdir = $item.WorkingDir.S
                    $appId = $item.ID.S
                    $appicon = $item.IconData.S
    
                    Write-Log "Adding $appname to the dynamic application catalog..."
    
                    #Because user is a member of the group, add the application to the AppStream catalog dynamically
                    $applist = New-Object -TypeName AppStream.ApplicationCatalogService.Model.Application("$appId", "$appname", "$apppath", "$appicon")
                    $getappreq = New-Object -TypeName Appstream.ApplicationCatalogService.Model.AddApplicationsRequest($usersid, $applist) 
                    $addapp = $thriftclient.AddApplications($getappreq)
                 }
            }
    
            Close-Thrift
            break
        }
        default {
            Write-Log "Invalid AppCatalog Method. Exiting"
            Exit
        }
    }
    
    Exit
    

Create Active Directory groups for Application Assignment

This solution relies upon a consistent naming convention for the Active Directory groups that control the visibility of application via the dynamic app framework. These groups should reside in an Active Directory Organizational Unit (OU) in which the AppStream 2.0 computer accounts have read access. The PowerShell script that drives this solution will be ran in the SYSTEM context in order to use the Thrift DLL.

The example script provided stores the prefix to search for in a variable at the top of the script: $ADGroupPrefix = "App Grant*" 

Using this prefix, example Active Directory groups would be “App Grant – PuTTY” and “App Grant – FireFox”.

Update this value in the script, AS2_DAF_Login.ps1, to match the naming scheme for the Active Directory groups in your environment, then create and populate the groups as needed.

Create Windows Scheduled Task to Trigger Script

  1. On the Image Builder, launch Task Scheduler.
  2. Within the Task Scheduler Library, right click and select Create Task.
  3. Give the task a descriptive name such as “AS2DAF Logon Task”.
  4. Under Security options, select Change User or Group.
  5. In the Select User or Group dialog box, enter SYSTEM into the box and click OK.

Scheduled Task Details

  1. On the Triggers tab, select New Trigger.
  2. In the Begin the task dropdown, select At log on, click OK.
  3. On the Actions tab, select New.
  4. Leave the Action as Start a program. In the Program/script box enter the path to the PowerShell executable: C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
  5. In Add arguments, enter:

-NoProfile –NonInteractive -ExecutionPolicy Bypass -Command "& 'C:\AS2DAF\AS2_DAF_Login.ps1'"

Ensure that the path and script name match what was put in place in the earlier step.  As a best practice, once this solution is fully tested and verified in your lab or test environment, considering removing -ExecutionPolicy Bypass from the argument list and signing the PowerShell script. Please refer to the official documentation on PowerShell execution policies and script signing.

  1. Click OK to complete the wizard.

Option 1) Local CSV File for DAF Application Information

The default method for supplying the information required by the dynamic app framework DLL files is to read that information from a CSV file stored within the AppStream 2.0 image.

  1. On the Image Builder, create a CSV file inside the AS2DAF directory called apps.csv.

The CSV file is read by the PowerShell script on user logon and contains the following information for each application:

    • The Active Directory group for access to that application: ADGroup
    • A unique ID number for each application in the CSV: Id
    • The applications display name (how it appears for users in the catalog): DisplayName
    • The full path to the applications executable (can be a local or a UNC/remote path): LaunchPath
    • The icon image file to display in the catalog (stored as a Base64String): IconData
    • (Optional) Any additional launch parameters passed to the program: LaunchParams
    • (Optional) The working directory to launch the application in: WorkingDir

The Base64 icon string can be created by converting an image file with PowerShell by using the .NET method Convert.ToBase64String.

$iconimagedata = [convert]::ToBase64String((get-content "Path\to\image\icon.png" -Encoding byte))

  1. Populate the CSV file, one line for each application, using the above guidelines.

Applications CSV Example

Option 2) S3 Hosted CSV File for DAF Application Information

The supplied example PowerShell script can be modified to obtain the required application information from a CSV file stored within S3, instead of locally within the AppStream 2.0 image. Storing the file outside of the image itself allows greater flexibility to make changes or additions without having to update the image itself. This is especially useful for changing settings on existing applications such as the Active Directory group assignment or launch parameters, and for adding additional applications such as those hosted on a UNC path or network share.

  1. Edit the AS2_DAF_Login.ps1 script copied onto the Image Builder previously.
    • Update the variable $AppCatalogMethod from “local” to "s3".
    • Update the variable $AppCatalogS3Bucket to match the S3 bucket name created below.
    • Update the variable $AppCatalogS3FileName to match the CSV file name uploaded to the bucket, the default is s3_apps.csv.
  2. Launch the S3 console, and click Create bucket.
    • Bucket name: Enter a unique bucket name
    • Region: select the Region the AppStream 2.0 fleet resides in for best performance
    • Enable versioning, encryption, and tags per your account standards.
    • Click Create bucket.
  3. Click on the newly created bucket to browse inside it.
  4. Create a new CSV file name s3_apps.csv on your local machine. The CSV file is read by the PowerShell script on user logon and contains the following information for each application:
    • The Active Directory group for access to that application: ADGroup
    • A unique ID number for each application in the CSV: Id
    • The applications display name (how it appears for users in the catalog): DisplayName
    • The full path to the applications executable (can be a local or a UNC/remote path): LaunchPath
    • The icon image file to display in the catalog (stored as a Base64String): IconData
    • (Optional) Any additional launch parameters passed to the program: LaunchParams
    • (Optional) The working directory to launch the application in: WorkingDir

The base64 icon string can be created by converting an image file with PowerShell by using the .NET method Convert.ToBase64String.

$iconimagedata = [convert]::ToBase64String((get-content "Path\to\image\icon.png" -Encoding byte))

  1. Populate the CSV file, one line for each application, using the above guidelines.

Applications CSV Example

  1. Upload the CSV file into the S3 bucket.

To allow the Image Builder and AppStream 2.0 fleet instances to retrieve the CSV file stored in S3, you must create or modify an IAM policy with the required permissions and assign them to AppStream 2.0 via the fleet IAM role setting.

  1. Launch the IAM console and select Policies.
  2. Select Create policy.
  3. Click the JSON tab and the paste in the below policy. Ensure that the Bucket Name placeholder has been updated with the name of the bucket created above.
    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "VisualEditor0",
                "Effect": "Allow",
                "Action": [
                    "s3:GetObject",
                    "s3:ListBucket"
                ],
                "Resource": "arn:aws:s3:::<BUCKET-NAME>"
            }
        ]
    }
  4. Click Next: Tags.
  5. Tag as required for your organization and select Next: Review.
  6. Name the policy (AS2_DAF_Read_S3), and click Create policy.
  7. Add this policy to an existing Role that is already assigned to your AppStream 2.0 fleets, or continue on from this step to create and assign a new Role.
  8. Select Roles and Create role.
  9. Select the AppStream 2.0 use case, and click Next: Permissions.
  10. Search for and select the AS2_DAF_Read_S3 policy created above. Click Next: Tags.
  11. Tag as required for your organization and select Next: Review.
  12. Name the role (AS2_DAF_Fleet_Role). Select Create role.
  13. Navigate to the AppStream 2.0 console and select Fleets.
  14. Select the radio button next to the fleet that will run the image with the dynamic application framework enabled, and ensure the fleet is in the Stopped state.
  15. From the Action dropdown, select Edit.
  16. In the IAM role dropdown, select the AS2_DAF_Fleet_Role (or your existing role to which the AS2_DAF_Read_S3 policy was attached to).
  17. Select Update Fleet.

Option 3) DynamoDB Table for DAF Application Information

The supplied example PowerShell script can be modified to obtain the required application information from a DynamoDB table instead of the locally stored CSV file.

  1. Edit the AS2_DAF_Login.ps1 script copied onto the Image Builder previously.
    • Update the variable $AppCatalogMethod from “local” to "dynamodb".
    • Update the variable $AppCatalogDynamoDBName to match the DynamoDB table created below, the default name is as2_daf_appcatalog.
  2. Launch the DynamoDB console, and select Create table to construct a new table to contain the information about each application required by the dynamic app framework.
    • Table name: as2_daf_appcatalog
    • Partition key: ID, type String
    • Check the box for Add sort key.
    • Enter sort key: ADGroup
  3. Click Create.
  4. Once created, click on the Items tab and then Create item.
  5. Create a new item for every application, with the following columns (all type String):
    • ID: a unique ID number for each application
    • ADGroup: the Active Directory group for access to that application
    • DisplayName: the applications display name (how it appears for users in the catalog)
    • LaunchPath: the full path to the applications executable (can be a local or a UNC/remote path)
    • IconData: the icon image file to display in the catalog (stored as a Base64String)
    • (Optional) LaunchParams: any additional launch parameters passed to the program
    • (Optional) WorkingDir: the working directory to launch the application in

DynamoDB Example

To allow the Image Builder and AppStream 2.0 fleet instances to access the data contained in the DynamoDB table, you must create or modify an IAM policy with the required permissions and assign them to AppStream 2.0 via the fleet IAM role setting.

  1. Launch the IAM console and select Policies.
  2. Select Create policy.
  3. Click the JSON tab and the paste in the below policy. Ensure that the Region and Account number placeholders have been updated with accurate values, and that the name of the table matches what was created above.
    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "VisualEditor0",
                "Effect": "Allow",
                "Action": [
                    "dynamodb:DescribeTable",
                    "dynamodb:GetItem",
                    "dynamodb:Scan",
                    "dynamodb:Query"
                ],
                "Resource": "arn:aws:dynamodb:<AWS-REGION-ID>:<AWS-ACCOUNT-NUMBER>:table/as2_daf_appcatalog"
            }
        ]
    }
  4. Click Next: Tags.
  5. Tag as required for your organization and select Next: Review.
  6. Name the policy (AS2_DAF_DynamoDBReadAccess), and click Create policy.
  7. Add this policy to an existing Role that is already assigned to your AppStream 2.0 fleets, or continue on from this step to create and assign a new Role.
  8. Select Roles and Create role.
  9. Select the AppStream 2.0 use case, and click Next: Permissions.
  10. Search for and select the AS2_DAF_DynamoDBReadAccess policy created above. Click Next: Tags.
  11. Tag as required for your organization and select Next: Review.
  12. Name the role (AS2_DAF_Fleet_Role). Select Create role.
  13. Navigate to the AppStream 2.0 console and select Fleets.
  14. Select the radio button next to the Fleet that will run the image with the dynamic application framework enabled, and ensure the Fleet is in the Stopped state.
  15. From the Action dropdown, select Edit.
  16. In the IAM role dropdown, select the AS2_DAF_Fleet_Role (or your existing role to which the AS2_DAF_DynamoDBReadAccess policy was attached to).
  17. Select Update Fleet.

Install Applications, Create Image, and Test

Install the required applications into the AppStream 2.0 Image Builder as usual. Those applications that were added to the application CSV file or DynamoDB table should not be manually added via the Image Assistant when creating the image. You can install any additional applications that will not be controlled by the dynamic app framework, and add those via the Image Assistant for all users. Ensure that the Add dynamic app providers box is checked on the first screen of the Image Assistant, and proceed to create the image as usual.

Enable Dynamic App Provider

Once the image creation process is completed and the image is in the Available state, assign it to a fleet, and start the fleet. Log into the fleet as a domain user that is a member of at least some of the Active Directory groups controlling application access. After successfully logging on, you should see a spinning wheel icon along with “Checking for applications” in the AppStream 2.0 portal indicating that the dynamic app framework is active and is building the dynamic application catalog. Shortly after, the icons for the dynamic applications should appear in the catalog for the user.

AppStream Launcher with DAF

The PowerShell script will output its actions to a log file stored in the user’s Documents directory (C:\Users\%USERNAME%\Documents\Logs\DAFClient). Refer to this log file for information while troubleshooting any issues.

Conclusion

You now have an AppStream 2.0 image, with dynamic apps enabled, allowing for a consolidation in the number of fleets required to support a diverse set of applications. You now have the option to continue to utilize existing, or new, Active Directory groups to control the visibility of applications within your fleets.

You can take this a step further and utilize an application layering or masking solution to completely prevent unauthorized access to applications in addition to revealing them in the application catalog. See the following blog posts for examples on how to accomplish this: Simplify Amazon AppStream 2.0 image management with Application Masking and Using Microsoft AppLocker to manage application experience on Amazon AppStream 2.0.