O365 – Limit App Only Permissions to Specific mailboxes

I was recently tasked with providing access to a SaaS Business Spend Management applications access to specific mailboxes in an O365 tenant. This lead me in reading and understanding how this can be achieved and I’ve detailed it in this post.

Before we go further, it is important to understand the differences between Application permissions and Delegated permissions supported by the Microsoft identity platform:

  • Delegated permissions allow an Azure AD application perform actions on behalf of the signed-in user. The user or an administrator consents to the permissions that the app requests. The app has permission to act as the signed-in user when it makes API calls to the target resource.
  • Application permissions allow an Azure AD application run as background services or daemon apps without the presence of a signed-in user.

This SaaS application needed to access emails from a shared mailbox in the tenant. This permission was needed by the SaaS application to read invoices and bills sent to a shared mailbox.

This would be application permissions. Applications like these use OAuth 2.0 Client credentials grant flow to authenticate. So, creating an Azure AD application and granting application permissions as mail.read should solve what we are trying to achieve..right? but wait, there this more. Adding mail.read application permissions allows this app, ability to read mail in all mailboxes in an organization in Exchange Online.

In my above statement, ‘ability to read mail in all mailboxes’ should make any mail administrator scream. Well, that is the problem statement and what we have to solve here.

OAuth 2.0 Client credentials grant flow

In this scenario, I have to limit an Azure AD app to only access specific mailboxes and not all mailboxes in the tenant. Below diagram has a high-level overview of the thought process on how I’m planning to implement this.

For sake of explanation, I have a mailbox named ‘U.S – Marketing’ and I have to grant mail.read permission to the SaaS BSM application. I’ll create a new Azure AD application, add mail.read permission. Next, create a mail-enabled security group named ‘US.Marketing.Mailbox.Access’, add ‘U.S – Marketing’ to the group and then apply the application access policy to restrict access to the mail-enabled security group.

First step is to create a new Azure AD application, add mail.read API permissions and grant admin consent. Yes. we can do this in the Azure AD portal in the App registrations blade but where is the fun in that. If you are in a hurry and need to get this done, the Azure AD portal is the best way as there is lot more information you need to determine for the Add-AzADAppPermission cmdlet’s parameters.

Before proceeding further, make sure you are connected to Azure AD PowerShell with a global admin account.

You can use the below lines in PS to achieve this. The az ad app is part of Azure CLI and not a PS cmdlet. You’ll need to have Azure CLI installed and do az login as well before running this.

$appname = Read-Host "Enter your Azure AD Application's Display Name"
$ObjID = New-AzureADApplication -DisplayName $appname | Select ObjectId
Add-AzADAppPermission -ObjectId $ObjID.ObjectId -ApiId 00000002-0000-0ff1-ce00-000000000000 -PermissionId 810c84a8-4a9e-49e6-bf7d-12d183f40d01 -Type Role
Start-Sleep -Seconds 60
az ad app permission admin-consent --id $ObjID.ObjectId

For the Add-AzADAppPermission cmdlet above, How I determined and arrived with the ApiId and PermissionId is covered in a different blogpost here.

Checking the result in Azure AD portal –> App Registration blade,

Second step is to create an ApplicationAccessPolicy with the policy scope set to the mail-enabled security group,

$appname = Read-Host "Enter your Azure AD Application's Display Name"
$mailbox = Read-Host "Enter mail-enabled security group's address"
$Desc = Read-Host "Enter Description"
$id = Get-AzureADApplication -Filter "DisplayName eq '$appname'"
New-ApplicationAccessPolicy -AppId $id.AppId -PolicyScopeGroupId $mailbox -AccessRight RestrictAccess -Description $Desc

To view the list of all application access policies, Get-ApplicationAccessPolicy cmdlet can be used:

Get-ApplicationAccessPolicy | Format-Table -Auto ScopeName, AccessRight, IsValid, Description

What we’ve done so far is, provided an application permissions to read all emails in a specific mailbox. As we applied the scope to a mail-enabled security group, we add this ‘specific mailbox’ I mentioned in my earlier statement to this mail-enabled security group. To test access right of an application to a specific mailbox or a user, Test-ApplicationAccessPolicy cmdlet can be used:

$appname = Read-Host "Enter your Azure AD Application's Display Name"
$mailbox = Read-Host "Enter email address to test access"
$id = Get-AzureADApplication -Filter "DisplayName eq '$appname'"
Test-ApplicationAccessPolicy -AppID $id.AppId -Identity $mailbox

In the below examples with screenshots, the ‘U.S – Marketing’ mailbox is part of the mail-enabled security group named ‘US.Marketing.Mailbox.Access’. Whereas ‘teams-admin’ mailbox is not, you can see the AccessCheckResult output.

While I was experimenting with the application access policies, I noticed that the changes made to it can take some time to show results. So, if you are following the steps and it still didn’t work..give it some time.

Hope this helped you in limiting application permissions to specific mailboxes in your tenant.

Thank you for stopping by. ✌

Office 365 – License Reporting using PowerShell – Updated

Reporting on O365 licenses is crucial and is necessary to keep track. I’m sure just like me most administrators get asked to generate reports on how the current license state on the tenant. In this post, I have this script I’ve put together just for this purpose.

The O365 portal does provide few options to export the data but this script generates the below reports,

  • O365 license usage
  • All enabled and licensed users report
  • All unlicensed users report
  • All disabled and licensed users report

This report can also be scheduled to run if you already use a mechanism to store your credentials securely and pass it on to your PS scripts.

I use the ImportExcel PowerShell module for this script,

Install-Module -Name ImportExcel

Before proceeding further, you must first connect to your MS online service. To do so, run the cmdlet Connect-MsolService at the Windows PowerShell command prompt. You will then be prompted for your credentials.

$Msolcred = Get-credential
Connect-MsolService -Credential $MsolCred

$xlsxPath = ".\Office365LicenseReport_$((Get-Date).ToString("MMddyyyy")).xlsx"
$LicNames = Get-Content -raw ".\FriendlyLicenseName.txt" | ConvertFrom-StringData

Get-MsolAccountSku | foreach {
            $ActiveUnits = $_.ActiveUnits
            $ConsumedUnits = $_.ConsumedUnits
            $LicenseItem = $_.AccountSkuId -Split ":" | Select-Object -Last 1
            $FriendlyName = $LicNames[$LicenseItem]

            [PSCustomObject]@{
                    'License' = $FriendlyName;
                    'Active Units' = $ActiveUnits;
                    'Consumed Units' = $ConsumedUnits
            }
} | Export-Excel -Path $xlsxPath -WorksheetName "Licensed_State" -TableStyle Medium16 -AutoSize -Append

Get-MsolUser -All -EnabledFilter EnabledOnly | where {$_.IsLicensed -eq $true} | Select-Object -ExpandProperty Licenses DisplayName,UserPrincipalName,Title,Department,UsageLocation | Select DisplayName,UserPrincipalName,Title,Department,UsageLocation,AccountSkuId | foreach {
            $DisplayName = $_.DisplayName
            $UPN = $_.UserPrincipalName
            $JobTitle = $_.Title
            $Department = $_.Department
            $UsageLoc = $_.UsageLocation
            $LicenseItem = $_.AccountSkuId -Split ":" | Select-Object -Last 1
            $FriendlyName = $LicNames[$LicenseItem]

            [PSCustomObject]@{
                    'Display Name' = $DisplayName;
                    'User Principal Name' = $UPN;
                    'License Plans' = $FriendlyName;
                    'JobTitle' = $JobTitle;
                    'Department' = $Department;
                    'Usage Location' = $UsageLoc
            }
}  | Export-Excel -Path $xlsxPath -WorksheetName "Enabled_Licensed_Users" -TableStyle Medium16 -AutoSize -Append

Get-MsolUser -All -UnlicensedUsersOnly | foreach {
            $DisplayName = $_.DisplayName
            $UPN = $_.UserPrincipalName
            $JobTitle = $_.Title
            $Department = $_.Department

            [PSCustomObject]@{
                    'Display Name' = $DisplayName;
                    'User Principal Name' = $UPN;
                    'JobTitle' = $JobTitle;
                    'Department' = $Department
            }
} | Export-Excel -Path $xlsxPath -WorksheetName "UnLicensed_Users" -TableStyle Medium16 -AutoSize -Append

Get-MsolUser -All -EnabledFilter DisabledOnly | where {$_.IsLicensed -eq $true} | Select-Object -ExpandProperty Licenses DisplayName,UserPrincipalName,Title,Department,UsageLocation | Select DisplayName,UserPrincipalName,Title,Department,UsageLocation,AccountSkuId | foreach {
            $DisplayName = $_.DisplayName
            $UPN = $_.UserPrincipalName
            $JobTitle = $_.Title
            $Department = $_.Department
            $UsageLoc = $_.UsageLocation
            $LicenseItem = $_.AccountSkuId -Split ":" | Select-Object -Last 1
            $FriendlyName = $LicNames[$LicenseItem]

            [PSCustomObject]@{
                    'Display Name' = $DisplayName;
                    'User Principal Name' = $UPN;
                    'License Plans' = $FriendlyName;
                    'JobTitle' = $JobTitle;
                    'Department' = $Department;
                    'Usage Location' = $UsageLoc
            }
} | Export-Excel -Path $xlsxPath -WorksheetName "Disabled_Licensed_Users" -TableStyle Medium16 -AutoSize -Append

I used this link to create a file to do a lookup of the ID which is the output from the AccountSkuId and convert it into a friendly name. This list is subject to change but you can download the ‘FriendlyLicenseName.txt‘ from this below link.

Place this file in the same location as you have the PS script or modify script accordingly.

I found it useful that this script’s output can be readily used to generate Pivot tables or charts. If you wish, you can also generate it straight from the script using -IncludePivotTable

Hope this script was helpful in determining the current license state in your O365 environment.

Thank you for stopping by. ✌

Azure AD – Create Security Groups and Add Members using PowerShell

Azure AD security groups can be used to manage member and computer access to shared resources for a group of users. A security group can have users, devices, groups and SPNs as its members. In this post, I’ll go over steps on how to create a Azure AD security group, add users to one group and also bulk import users to one group and multiple groups.

Make sure you are connected to Azure AD,

$credential = Get-Credential
Connect-AzureAD -credential $credential

To create an Azure AD security group:

$GroupName = Read-Host "Enter name for Azure AD group"
New-AzureADGroup -DisplayName $GroupName -MailEnabled $false -SecurityEnabled $true -MailNickName "NotSet"

To create multiple Azure AD security groups:

Note: csv file has name,description as column headers

$groups = import-csv "C:\tmp\AzureAD Groups\groups.csv"

Foreach($group in $groups) {
New-AzureADGroup -DisplayName $group.name -Description $group.description -MailEnabled $false -SecurityEnabled $true -MailNickName "NotSet"
}

To add an user and an owner to an existing group:

$Group = Read-Host "Enter name of Azure AD group"
$User = Read-Host "Enter username of user to be added"
$Owner = Read-Host "Enter username of group owner"

$GroupObj = Get-AzureADGroup -SearchString $Group
$UserObj = Get-AzureADUser -SearchString $User
$OwnerObj = Get-AzureADUser -SearchString $Owner

Add-AzureADGroupMember -ObjectId $GroupObj.ObjectId -RefObjectId $UserObj.ObjectId
Add-AzureADGroupOwner -ObjectId $GroupObj.ObjectId -RefObjectId $OwnerObj.ObjectId

To display members of a security group:

$Group = Read-Host "Enter name of Azure AD group to display members"
$GroupObj = Get-AzureADGroup -SearchString $Group
Get-AzureADGroupMember -ObjectId $GroupObj.ObjectId

To display owners of a security group:

$Group = Read-Host "Enter name of Azure AD group to display owner(s)"
$GroupObj = Get-AzureADGroup -SearchString $Group
Get-AzureADGroupOwner -ObjectId $GroupObj.ObjectId

To bulk add multiple users to a specific group:

Note: csv file has UserPrincipalName as column header

$Group = Read-Host "Enter name of Azure AD group to add users"
$GroupObj = Get-AzureADGroup -SearchString $Group
$Members = Import-csv "C:\tmp\GroupMembers.csv"
Foreach($Member in $Members) {

$User = $Member.UserPrincipalName
Write-Progress -Activity "Adding user.." -Status $User

Try {
$UserObj = Get-AzureADUser -SearchString $User
Add-AzureADGroupMember -ObjectId $GroupObj.ObjectId -RefObjectId $UserObj.ObjectId
}
catch {
Write-Host "Error occured while adding $User" -ForegroundColor Red
}
}

To bulk add multiple users to multiple groups. This below script checks to see if the user is already part of the Azure AD group and returns an error.

Note: csv file has UserPrincipalName,Group as column headers

$list = Import-csv "C:\tmp\UsersGroups.csv"
Foreach($entry in $list) {

$User = $entry.UserPrincipalName
$Group = $entry.Group
$UserObj = Get-AzureADUser -SearchString $User
$GroupObj = Get-AzureADGroup -SearchString $Group
$GroupMembers = (Get-AzureADGroupMember -ObjectId $GroupObj.ObjectId).UserPrincipalName

If ($User -notin $GroupMembers) {
    Add-AzureADGroupMember -ObjectId $GroupObj.ObjectId -RefObjectId $UserObj.ObjectId
    Write-Host "$User added to $Group" -ForegroundColor Green
    }
    else {
    Write-Host "$User already in $Group" -ForegroundColor Red
    }
}

Hope this was helpful for you in exploring Azure AD security groups.

Thank you for stopping by. ✌

Active Directory – Set ‘Manager Can Update Membership List’ with PowerShell

AD group management usually is delegated in most organizations. Usually one of helpdesk’s responsibility or sometime a department manager decides who is member of her or his team at any given time. These users can be non-IT users in several situations.

To serve this purpose, a user can be designated as manager in the Managed By tab of the security group using Active Directory Users and Computers (ADUC) console. Placing a checkmark next to Manager can update membership list allows the user to update the group member by adding or removing users from the security group.

The Manager can update membership list option modifies the Access Control List (ACL) of the security group which we can see in the Security tab, in Advanced.

Setting the Managed by is fairly straight-forward. We can use the Set-ADGroup cmdlet to update the value. We can use the below code to update manager for a group:

$grpname = Read-Host "Enter AD Group's name"
$Managername = Read-Host "Enter AD user's name"
Get-ADGroup -Identity $grpname | Set-ADGroup -ManagedBy "$Managername"

But this doesn’t enable the Manager can update membership list option. To allow this user who is set as this group’s manager, we need to add an AccessRule for this user to the ACL of the group. I’m using this below process to put together a script,

In this below script, We get the user and current ACL of the group. And then the user who will be the manager. And to build the new ACL, we obtain the SID string, then set control type and AD permission. We also need to specify the System ID GUID of the Member attribute and this ACL need to be applied to this specific attribute (Member). Use all the variables to build a new rule, add the rule to the existing rule. And finally use Set-Acl cmdlet to apply new ACL to the Group. Yup!.. that easy!! 🤷‍♂️

$grpname = Read-Host "Enter AD Group's name"
$Grp = Get-ADGroup -Identity $grpname
$GroupACL = Get-Acl AD:\$Grp

$Managername = Read-Host "Enter AD user's name"
$Manager = Get-ADUser -Identity $Managername
$UserSid = New-Object System.Security.Principal.NTAccount ($Manager.SamAccountName)

$Rights = [System.DirectoryServices.ActiveDirectoryRights]::WriteProperty
$Control = [System.Security.AccessControl.AccessControlType]::Allow
$Guid = [guid]"bf9679c0-0de6-11d0-a285-00aa003049e2"
$Rule = New-Object System.DirectoryServices.ActiveDirectoryAccessRule ($UserSid, $Rights, $Control, $Guid)
$GroupACL.AddAccessRule($Rule)
Set-Acl -AclObject $GroupACL -path AD:\$Grp
Get-ADGroup -Identity $Grp | Set-ADGroup -ManagedBy "$Manager"
Write-Host $Manager.Name set as manager on $group.groupname

What if you have to update multiple groups with a user as manager to each. I didn’t have enough time to make a detailed one and the below script will serve the purpose. I’d probably include checks to make sure the group and the user exists in AD.

Note: csv file has groupname,managerSamname as column headers

$groups = import-csv "C:\Scripts\list.csv"

foreach ($group in $groups){
        $Grp = Get-ADGroup -Identity $group.groupname
        $GroupACL = Get-Acl AD:\$Grp
        $Manager = Get-ADUser -Identity $group.managerSamname
        $UserSid = New-Object System.Security.Principal.NTAccount ($Manager.SamAccountName)
        $Rights = [System.DirectoryServices.ActiveDirectoryRights]::WriteProperty
        $Control = [System.Security.AccessControl.AccessControlType]::Allow
        $Guid = [guid]"bf9679c0-0de6-11d0-a285-00aa003049e2"
        $Rule = New-Object System.DirectoryServices.ActiveDirectoryAccessRule ($UserSid, $Rights, $Control, $Guid)
        $GroupACL.AddAccessRule($Rule)
        Set-Acl -AclObject $GroupACL -path AD:\$Grp
        Get-ADGroup -Identity $Grp | Set-ADGroup -ManagedBy "$Manager"
        Write-Host $Manager.Name set as manager on $group.groupname
}

Hope this post helped you in bulk setting group managers in AD.

Thank you for stopping by.✌

Amazon AppStream 2.0 – Increase Size of the Application Settings VHD

When we enable application settings persistence, the users’ Windows settings and application customizations are to a VHD file and is stored in a S3 bucket the same account we created the AppStream 2.0 stack. For every AWS Region, AppStream 2.0 creates a bucket in that account that is unique to the account and the Region. All application settings configured by the users are stored in the bucket for that Region.

There are no special configuration tasks needed to manage these S3 buckets as they are fully managed by the AppStream 2.0 service. The VHD file that is stored in each bucket is encrypted in transit using S3’s SSL endpoints and at rest using AWS Managed keys.

The buckets are named in a specific format:

appstream-app-settings-region-code-account-id-without-hyphens-random-identifier

The path for the folder where the settings VHD is stored in the S3 bucket uses the following structure:

bucket-name/Windows/prefix/settings-group/access-mode/user-id-SHA-256-hash
  • bucket-name = Name of the S3 bucket in which users’ application settings are stored
  • prefix = Windows version-specific prefix. Example v6 for Server-2016, Server-2019
  • settings-group = The settings group value from when we create the stack
  • access-mode = Identity method of the user
    • custom for the AppStream 2.0 API or CLI
    • federated for SAML
    • userpool for user pool users
  • user-id-SHA-256-hash = The user-specific folder name. Created using a SHA-256 hash hexadecimal string generated from the user ID

I use Notepad++ to determine the SHA-256 hash for a given user. When you have more than 1 user(😁) you’ll need to determine the user’s folder and this is an extra step that you need to go through.

To determine a user’s SHA-256 Hash using Notepad++:

  1. Click on Tools and SHA-256
  2. Click Generate

I leave the Treat each line as a separate string checked when I add multiple users. For a domain user, make sure the type the user ID in the UPN format, testuser@mydomain.com.

Note: This is case-sensitive. See screenshot for example.

The default VHD maximum size is 1 GB. There will be scenarios where the user requires additional space for application settings, we can download the users’ VHD to a Windows computer to expand it. Then, replace the current VHD in the S3 bucket with the expanded one.

Note: Don’t do this when the user has an active streaming session.

I have the TestUser01’s VHD that I’m going to download, expand and upload back to the S3 bucket.

  1. Locate and select the folder that contains the VHD
    • I used the SHA-256 hash generated earlier, copied it from Notepad++ to my clipboard and while in the S3 folder, I did a search or browser’s ctrl+f also works
  2. Download the Profile.vhdx file to a directory onto a Windows computer
    • Do not close the browser after the download completes
      • Easier to use the same session to upload the file back
  1. Open the command prompt as an administrator, and diskpart
  2. Type the following commands to select the vhd file and expand it
    • I’ve stored the VHDX file in C:\tmp\VHDs
select vdisk file="C:\tmp\VHDs\profile.vhdx"
expand vdisk maximum=2000
  1. Type the following Diskpart commands to find and attach the VHD, and display the list of volumes:
select vdisk file="C:\tmp\VHDs\profile.vhdx"
attach vdisk
list volume

The volume gets mounted as a drive and as you may notice, it is easier to determine the VHD with the volume label.

  1. Type the following command:
    • Corresponds to the number in the list volume output
    • In my case Volume 7
select volume ##
  1. Type the following command:
extend
  1. To confirm that the size of the partition on the VHD increased as expected
select vdisk file="C:\tmp\VHDs\profile.vhdx"
list volume
  1. To detach the VHD so that it can be uploaded
detach vdisk
  1. Return to the browser with the Amazon S3 console, choose Upload, Add files, and then select the enlarged VHD in file explorer
  2. Click Upload

The page shows upload progress,

Next time the user starts a streaming session from a fleet on which application settings persistence is enabled with the applicable settings group, the larger application settings VHD is available.

Kind of a related note that I learned while working with application settings persistence is, disabling application settings persistence or deleting the stack does not delete any VHDs stored in the S3 bucket. These VHDs must be deleted manually from the Amazon S3 console or API.

Hope you found this post useful in increasing the size of the application settings VHD in AppStream 2.0.

Thank you for stopping by.✌