Tag Archives: powershell

Insufficient privileges when setting a user license

Set-MGUserLicense allows administrators to utilize Microsoft Graph to set a users licenses. https://learn.microsoft.com/en-us/powershell/module/microsoft.graph.users.actions/set-mguserlicense?view=graph-powershell-1.0 Set-MGUserLicense leverages the user:AssignLicense graph interface to manage the users licenses. https://learn.microsoft.com/en-us/graph/api/user-assignlicense?view=graph-rest-1.0&tabs=http

I recently worked a customer escalation where when executing set-MGUserLicense the following error was noted:

Authorization_RequestDenied,Microsoft.Graph.PowerShell.Cmdlets.SetMgUserLicense_AssignExpanded
Set-MgUserLicense : Insufficient privileges to complete the operation.

Status: 403 (Forbidden)
ErrorCode: Authorization_RequestDenied

When Microsoft Graph returns an insufficient privileges error message this generally means that the permissions scopes required either do not exist on the user or graph application registration running the command. According to the user:AssignLicense interface documentation the minimum required permission is LicenseAssignment.ReadWrite.All and the maximum permissions are User.ReadWrite.All and Directory.ReadWrite.All.

Graph provides a method to review the context of the authentication as well as the scopes authorized using Get-MGContext. https://learn.microsoft.com/en-us/powershell/module/microsoft.graph.authentication/get-mgcontext?view=graph-powershell-1.0. When reviewing the scopes post authentication the following was displayed:

Get-MgContext | Select-Object -ExpandProperty Scopes

DeviceManagementManagedDevices.Read.All
UserAuthenticationMethod.Read.All
User.ReadWrite.All
Policy.ReadWrite.ApplicationConfiguration
Application.ReadWrite.All
Group.Read.All
Directory.ReadWrite.All
Directory.Read.All
User.Read.All
GroupMember.Read.All
DeviceManagementRBAC.Read.All
DeviceManagementManagedDevices.ReadWrite.All
Mail.Send
Organization.Read.All
AuditLog.Read.All
Policy.Read.All
DeviceManagementManagedDevices.PrivilegedOperations.All

The maximum scopes required are present for the graph connection, yet the insufficient privileges error continues. The following command was being utilized to set the users licenses:

Set-MGUserLicense -userID 'ObjectID' -AddLicenses @{SkuId = '38b434d2-a15e-4cde-9a98-e737c75623e1'} -RemoveLicenses @()

When reviewing the SKU ID this SKU ID is associated with a Visio Plan 2 sku. (https://learn.microsoft.com/en-us/entra/identity/users/licensing-service-plan-reference). If you pay close attention to the table, you will see that the product name is Visio Plan 2, but the String ID is Visio_Plan2_Dept. The insufficient privileges is actually a red herring in terms of how we would normally interpret this error. In this case the insufficient privileges are not derived from lacking graph scopes but rather from the fact that this sku cannot be directly assigned to a user. A department sku can only be assigned by the Microsoft License Manager which is a self-service license acquisition process by the user.

To correct the insufficient privileges error the correct Visio Plan 2 sku was specified.

Microsoft 365 GCC High Tenants and Missing Domains

Microsoft 365 provides a Government Community Cloud High offering for the U.S Government, contractors, and other organizations that qualify. As with our commercial or worldwide tenants many customers elect to add a vanity or custom domain to their M365 GCC-H tenant.

In a typical commercial tenant the vanity domains are managed through the domains tab of the Microsoft 365 Administration Center. Here is a sample instructions for commercial customers – https://learn.microsoft.com/en-us/microsoft-365/admin/setup/add-domain?view=o365-worldwide.

In the M365 Admin Center for GCC-H tenants the domains tab is missing. This is currently by design. When adding, verifying, or removing domains in a GCC-H tenant Microsoft Graph must be utilized. In this post I want to outline the manual steps for adding and verifying a domain in a GCC-H tenant.

Graph commands often span multiple PowerShell modules in order to achieve their work. Prior to running any graph commands, I recommend that all associated graph PowerShell modules be updated. This ensures that you do not have a mismatch, for example, between the authentication module and the identity module. Note: This process can take a long time depending on the number of graph modules you have installed.

Get-InstalledModule Microsoft.Graph.* | Update-InstalledModule -force -confirm:$FALSE

If you are new to graph or to ensure that you have the module necessary to perform domain work, the Microsoft.Graph.Identity.DirectoryManagement module must be installed.

Install-Module Microsoft.Graph.Identity.DirectoryManagement

When adding and verifying the domain there are three graph commands that will be utilized.

In order to run the following commands, the graph permission Domain.ReadWrite.All must either be consented to on the individual account or for the entire organization. Note: If you do not have global administrator privileges you will not be able to provide consent for graph scopes. It may be necessary for another administrator to perform the consent on your behalf.

The process to add a domain via graph:

$tenantID = "Entra Tenant ID for the GCC H" organization" #User Supplied
$scopes = "Domain.ReadWrite.All"
$environment = "USGov"

Connect-MgGraph -Environment $environment -TenantId $tenantID -Scopes $scopes
$domainID = "contoso.com"

$params = @{
	id = $domainID
}

new-MGDomain -bodyParameter $params

If the new-MGDomain command is successful the following return is expected.

Id          AuthenticationType AvailabilityStatus IsAdminManaged IsDefault IsInitial IsRoot IsVerified Manufacturer Mod
                                                                                                                    el
--          ------------------ ------------------ -------------- --------- --------- ------ ---------- ------------ ---
contoso.com Managed                               True           False     False     False  False

Once the domain has been added with new-MGDomain the DNS verification records are obtained. In the below example for the record type TXT you would use the value MS=ms41165256 or for the record type MX you would use ms41165256.msv1.invalid with preference 32767.

$dnsRecords = get-MGDomainVerificationDNSRecord -domainID $domainID
$dnsRecords | fl

Id                   : aceff52c-06a5-447f-ac5f-256ad243cc5c
IsOptional           : False
Label                : contoso.com
RecordType           : Txt
SupportedService     : Email
Ttl                  : 3600
AdditionalProperties : {[@odata.type, #microsoft.graph.domainDnsTxtRecord], [text, MS=ms41165256]}

Id                   : 5fbde38c-0865-497f-82b1-126f596bcee9
IsOptional           : False
Label                : contoso.com
RecordType           : Mx
SupportedService     : Email
Ttl                  : 3600
AdditionalProperties : {[@odata.type, #microsoft.graph.domainDnsMxRecord], [mailExchange, ms41165256.msv1.invalid],
                       [preference, 32767]}

These records are inserted into your commercial DNS provider for the domain you are adding. Please note that it can take several minutes for your new public DNS records to be accessible. You may consider using a third party tool or nslookup to validate that the DNS records are available prior to proceeding with the next step.

confirm-MGDomain -domainID $domainID

If the confirm is successful, the domain will be validated and available in the M365 GCC-H tenant.

To complete the integration of custom domains with Microsoft 365 services you have to publish additional DNS records. For GCC-H tenants these DNS records must be calculated manually. You can find resources for calculating these DNS records at https://learn.microsoft.com/en-us/microsoft-365/enterprise/dns-records-for-office-365-gcc-high?view=o365-worldwide.

Converting Direct Assigned Licenses to Group Assigned Licenses

In Microsoft 365 users may be assigned the same license both direct and through group-based licensing.  For organizations that are converting to group-based licensing this is not an uncommon scenario to ensure that users do not lose access to the service during the transition to group-based assignments.  When a license is assigned to user both direct and through group-based licensing only a single license is consumed. 

Managing direct and group based license assignment in the M365 Admin Center…

In the M365 Admin Center if a group-based license is assigned to the user the option to manage that individual license is greyed out.  A note is displayed “this is inherited by group-based licensing and can’t be changed here.  Manage group-based licenses from the Groups pivot in license details.”  If the user also had a direct assigned license this prevents the removal of the direct assigned license from the users’ properties.

The M365 Admin Center also allows expansion of individual licenses and displaying the users that have the license assigned.  Administrators also have the ability to assign and unassign licenses from this view. 

If you select the user that has both a direct and group-based license assignment and select unassign license an error is displayed.

When a user has both a direct and group-based license assignment the directly assigned license cannot be managed in the M365 Admin Center.

Using Graph to Manage Direct License Assignment

Get-MGUser provides the properties AssignedLicenses and LicenseAssignmentStates.  The license assignment states provides information regarding the licenses assigned to the user and how those licenses are assigned.

PS C:\> Get-MgUser -UserId "!LicenseTestUser2@domain" -Property AssignedLicenses, LicenseAssignmentStates, DisplayName | Select-Object DisplayName, AssignedLicenses -ExpandProperty LicenseAssignmentStates  | fl


DisplayName          : !LicenseTestUser2
AssignedLicenses     : {314c4481-f395-4525-be8b-2ec4bb1e9d91}
AssignedByGroup      : 519fe352-6f2d-4022-973e-ad72c5bcf63d
DisabledPlans        : {882e1d05-acd1-4ccb-8708-6ee03664b117}
Error                : None
LastUpdatedDateTime  : 2/3/2025 1:08:24 PM
SkuId                : 314c4481-f395-4525-be8b-2ec4bb1e9d91
State                : Active
AdditionalProperties : {}

DisplayName          : !LicenseTestUser2
AssignedLicenses     : {314c4481-f395-4525-be8b-2ec4bb1e9d91}
AssignedByGroup      :
DisabledPlans        : {}
Error                : None
LastUpdatedDateTime  : 2/3/2025 1:05:23 PM
SkuId                : 314c4481-f395-4525-be8b-2ec4bb1e9d91
State                : Active
AdditionalProperties : {}

In this case the user has two license assignments for the license 314c4481-f395-4525-be8b-2ec4bb1e9d91.  The first state has AssignedByGroup 519fe352-6f2d-4022-973e-ad72c5bcf63d and the second state shows no AssignedByGroup.  This demonstrates that the user has both a group and direct license assignment. 

With the inability to manage the direct assigned license in the M365 Admin Center if the desire is to remove the direct assigned license graph must be utilized.  Here is an example of removing the direct assigned license.  (See Using graph to modify group based licenses… | TIMMCMIC for how to build the body parameters section.)

#Establish the body parameters hash table.
$params = @{}
#Build the add licenses array
$addLicenses = @()

#Build the remove licenses array
$removeLicenses = @()
$disabledPlans = @()
$removeLicenses += "314c4481-f395-4525-be8b-2ec4bb1e9d91"
$params = @{"AddLicenses" = $addLicenses ; "RemoveLicenses" = $removeLicenses}
Set-MgUserLicense -UserId "!LicenseTestUser2@domain" -BodyParameter $params

When the command completes successfully repeating the get displays the following results.

PS C:\> Get-MgUser -UserId "!LicenseTestUser2@domain" -Property AssignedLicenses, LicenseAssignmentStates, DisplayName | Select-Object DisplayName, AssignedLicenses -ExpandProperty LicenseAssignmentStates  | fl


DisplayName          : !LicenseTestUser2
AssignedLicenses     : {314c4481-f395-4525-be8b-2ec4bb1e9d91}
AssignedByGroup      : 519fe352-6f2d-4022-973e-ad72c5bcf63d
DisabledPlans        : {882e1d05-acd1-4ccb-8708-6ee03664b117}
Error                : None
LastUpdatedDateTime  : 2/3/2025 1:08:24 PM
SkuId                : 314c4481-f395-4525-be8b-2ec4bb1e9d91
State                : Active
AdditionalProperties : {}

This output confirms that the user has a single license assignment state and that the license is assigned by a group.

Summary

When a direct and group-based license exists on the user the direct license assignment cannot be managed in the M365 Admin Center.  To migrate to group-based licensing the direct assigned license can be removed using Microsoft Graph. 

Entra / Azure: Searching for Microsoft IP Addresses

In a previous post I outlined a script that allows administrators to search for Microsoft 365 IP and URLs. As with Microsoft 365, Entra services also publish a list of IP addresses, and their service descriptions associated with each IP space.

Unlike Microsoft 365 the JSON files that contain this information are not made available through a web service. The files are made available through the Microsoft download catalog.

I have recently published a PowerShell module to the PowerShell gallery that automates the downloading of the Entra JSON files. Once the files have been downloaded, they may be utilized with the Office365IPAddresses script to locate an IP address within Entra services.

The AzureIPAddress script requires PowerShell 5.1. This is due to the methods utilized to capture the JSON files. To utilize the script open PowerShell 5.1 and run the following commands:

Install-Script AzureIPAddress
AzureIPAddress.ps1 -logFolderPath c:\temp

The script will locate the downloads for both Public and Government clouds and download the associated JSON files. They are placed in the logging directory in a folder called AzureIPAddress. In this example the folder is c:\temp\AzureIPAddress. (NOTE: The same log folder path must be utilized with the Office365IPAddress script in order to locate the Azure json files.)

To search for an IP address the Office365IPAddress script is utilized. Why is this not just included in the AzureIPAddress script? The ability to parse IPv4 and IPv6 addresses is more easily achieved with PowerShell 7. The same commands utilized in Office365IPAddress are not available in PowerShell 5.1. The commands in AzureIPAddress to download and parse the HTML files necessary to locate the JSON files are not available in PowerShell 7. I could have gotten creative and try to call PowerShell 7 from PowerShell 5.1 or vice versa, but that just adds potential complications. Keeping the script command separate but creating a dependency between them simplifies the process.

To search for the IP address run the following commands:

Install-Script Office365IPAddress
Install-Module PSWriteHTML
Office365IPAddress.ps1 -IPAddressToTest "52.247.151.193" -logFolderPath c:\temp -IncludeAzureSearch:$TRUE

During command execution all IP spaces associated with all Entra services in Public and Government cloud are searched. If the IP address is located in any service, the service information is logged and exported to XML. The log and XML file are contained in the specified log directory. An HTML file is also generated and displayed that provides the same information graphically for review.

If the IP address specified co-exists in any Microsoft 365 services, the service information is also displayed in the output.

This script should allow administrators to map IP addresses to Entra services.

Office 365 – Distribution List Migrations – Part 42

Increasing the success of Distribution List Migrations

When migrating a distribution list to Office 365 the DLConversionV2 module implements a normalization process for all recipients that are members of the distribution list.

The normalization process attempts to convert the recipient from an Active Directory object to an Exchange Online object. The goal of the normalization process is to ensure that we locate the correct recipient in Exchange Online when creating the distribution list and eliminate ambiguous recipient discovery. If an ambiguous recipient is located in Exchange Online this can lead to a migration failure and require the administrator to correct the condition post migration.

When a user is encountered on the properties of a distribution list being migrated the user is normalized by using the attribute msDS-ExternalDirectoryObjectID. In an Entra Connect Sync environment the msDS-ExternalDirectoryObjectID is populated with the value User_ExternalDirectoryObjectID. The ExternalDirectoryObjectID is the objectID associated with the user in EntraID. This same value is also stamped on all Exchange Online objects in the attribute ExternalDirectoryObjectID.

msDS-ExternalDirectoryObjectID = Entra ObjectID = Exchange Online ExternalDirectoryObjectID

PS C:\> Get-ADUser "DistinguishedName" -Properties msDS-ExternalDirectoryObjectID


DistinguishedName              : DistinguishedName
Enabled                        : False
GivenName                      :
msDS-ExternalDirectoryObjectID : User_8ac654f2-2125-4252-b9b0-d2219b9bb395
Name                           : Name
ObjectClass                    : user
ObjectGUID                     : ObjectGUID
SamAccountName                 : SamAccountName
SID                            : SID
Surname                        :
UserPrincipalName              : UserPrincipalName

When searching Exchange Online using the get-recipient command (or any similar get command) the ExternalDirectoryObjectID can be utilized as a recipient identifier.

In a default Entra Connect installation the attribute msDS-ExternalDirectoryObjectID is written back to Active Directory only on User object types. Groups and Contacts do not have the same attribute written back. When performing a migration if a Contact or Group is encountered the recipients are normalized to their Exchange Online counterparts through the object PrimarySMTPAddress. When locating recipients in Exchange Online get-recipient returns results for all objects that match the identifier specified. If a group has the PrimarySMTPAddress of group@contoso.com and a contact has a target address of group@contoso.com get-recipient will return two objects when specifying get-recipient -identity group@contoso.com. During a migration this can lead to a failure as more than object is returned when attempting to perform normalization.

To increase the efficiency of migrations and eliminate possible failures it is possible to enable writeback of the attribute msDS-ExternalDirectoryObjectID to both Contacts and Groups. Why is this not the default? When the writeback rules were created the goal was to optimize the number of changes written back to Active Directory. A decision was made to only populate this value on User objects as that is where it would most commonly be practical to have it. When writing back the same attribute to Groups and Contacts this allows the normalization process to extract the exact matching recipient in Exchange Online.

Migrators wishing to implement this efficiency may refer to a script published to the Powershell Gallery -> EnableCloudAnchor. This script allows administrators to create the necessary writeback rules in Entra Connect Sync to enable msDS-ExternalDirectoryObjectID on Contacts and Groups. When the script is executed two rules are created for each object type. The first rule is enabled and translates the EntraID value CloudAnchor to the msDS-ExternalDirectoryObjectID attribute. The second rule is created disabled and enables undoing all attributes written back by the script. This ensures that an undo operation is pre-staged should a rollback be necessary.

The script has several parameters to be specified:

  • ForestRootFQDN: This is the Active Directory forest root FQDN. This is utilized to locate the connector to enable writeback on.
  • StartingPrecedence: This is the starting precedence value for rule creation and must be specified as a value 0 – 99. For example, specifying a value of 25 will create the writeback rule at precedence 25 and the undo rule at precedence 26. This value is option. If not specified, the script will automatically locate the least two precedence available and automatically use them.
  • EnableContactProcessing: This enables rule creation for contacts and is the default for script execution. If enabling the rules for groups this value must be set to false.
  • EnableGroupProcessing: This enables rule creation for groups and by default is disabled. If this feature is enabled enableContactProcessing must be set to false.
  • LogFolderPath: The location of where script logging should occur.

To utilize the script on the Entra Connect server:

Install-Script EnableCloudAnchor

To create the rules for contacts utilizing auto discovered precedence:

EnableCloudAnchor.ps1 -forestRootFQDN contoso.local -logFolderPath c:\temp

To create the rules for contacts utilizing an administrator provided precedence:

EnableCloudAnchor.ps1 -forestRootFQDN contoso.local -startingPrecedence 24 -logFolderPath c:\temp

To create the rules for groups utilizing an auto discovered precedence:

EnableCloudAnchor.ps1 -forestRootFQDN contoso.local -enableContactProcessing:$FALSE -enableGroupProcessing:$TRUE -logFolderPath c:\temp

In Entra Connect the following rule is created to enable writeback of the cloud anchor attribute (only the relevant screens are displayed):

The following rule is also created in a disabled state to allow undoing the writeback operations:

If the need arises to undo the writeback the first rule would be deleted or placed into a disabled state. The disabled state flag would be unchecked on the second rule. The AuthoritativeNull value is utilized to clear an attribute entirely.

NOTE: Any modification to the rules in Entra Connect Sync will require a full synchronization to occur on the connector associated with the forest specified. This may add significant time to a synchronization option.

When the rules have successfully processed on an object the value Contact_ExternalDirectoryObjectID or Group_ExternalDirectoryObjectID may be found on the objects.

PS C:\> Get-ADObject DistinguishedName


DistinguishedName              : DistinguishedName
msDS-ExternalDirectoryObjectID : Contact_76b9cf05-510c-4fcc-a1f8-58e4cada17a6
Name                           : Name
ObjectClass                    : contact
ObjectGUID                     : ObjectGUID

When adding the msDS-ExternalDirectoryObjectID to Active Directory objects the normalization process may more accurately identify recipients in Exchange Online increasing the efficiency and success of migrations.

EntraID / Office 365: Using Graph Powershell to list domain DNS records…

In the Microsoft 365 Administration center administrators can review the domains they have verified and added to Microsoft 365 services. When a domain is validated and provisioned, the domain name services (DNS) records that are provisioned in Microsoft 365 are displayed for the administrator. This may include records such as the MX record for Exchange Online or the device management records for Intune support.

If the domains blade is not available to you in the Microsoft 365 Administration Center, it is possible to obtain these same records through Microsoft Graph. To obtain the DNS records the command Get-MGDomainServiceConfigurationRecord can be utilized. For API permissions necessary to utilize this command reference the following API permissions guidance.

Whenever utilizing Microsoft Graph commands I always recommend ensuring that the graph commands are running the latest non-preview version. To accomplish this task administrators may run:

Get-InstalledModule Microsoft.Graph.* | update-Module

If you have not yet installed any of the graph modules there are only two modules that are required in order to execute these commands.

Install-Module Microsoft.Graph.Authentication
Install-Module Microsoft.Graph.Identity.DirectoryManagement

Once the necessary modules have been installed or updated a connection to the Microsoft Graph endpoints must be established. In this example interactive authentication is utilized to establish the connection and prompt the user for credentials. The scopes parameter requests the least restrictive permissions to perform this operation. When connecting with graph command if a scope is not consented to for the particular user either consent can be granted by the user (assuming appropriate rights) or an administrator will be required to grant consent. I also provide the tenantID as a part of the connection. The tenantID can be obtained from the Entra ID portal associated with the domain. This ensures that the connection is made with the appropriate tenant.

connect-MGGraph -scopes "Domain.Read.All" -tenantID "00000000-0000-0000-0000-000000000000"

This is a sample consent screen

The command can then be issued using the domain name.

$records = Get-MgDomainServiceConfigurationRecord -DomainId domain.net

The information of particular interest is stored within the additionalProperties of each entry. The following command will help organize and interpret the information:

PS C:\> foreach ($record in $records) { $record.label ; $record.AdditionalProperties | ft}
domain.net

Key          Value
---          -----
@odata.type  #microsoft.graph.domainDnsMxRecord
mailExchange domain-net0c.mail.protection.outlook.com
preference   0


domain.net

Key         Value
---         -----
@odata.type #microsoft.graph.domainDnsTxtRecord
text        v=spf1 include:spf.protection.outlook.com -all


autodiscover.domain.net

Key           Value
---           -----
@odata.type   #microsoft.graph.domainDnsCnameRecord
canonicalName autodiscover.outlook.com


_sip._tls.domain.net

Key         Value
---         -----
@odata.type #microsoft.graph.domainDnsSrvRecord
nameTarget  sipdir.online.lync.com
port        443
priority    100
protocol    _tls
service     _sip
weight      1


sip.domain.net

Key           Value
---           -----
@odata.type   #microsoft.graph.domainDnsCnameRecord
canonicalName sipdir.online.lync.com


lyncdiscover.domain.net

Key           Value
---           -----
@odata.type   #microsoft.graph.domainDnsCnameRecord
canonicalName webdir.online.lync.com


_sipfederationtls._tcp.domain.net

Key         Value
---         -----
@odata.type #microsoft.graph.domainDnsSrvRecord
nameTarget  sipfed.online.lync.com
port        5061
priority    100
protocol    _tcp
service     _sipfederationtls
weight      1


domain.net

Key           Value
---           -----
@odata.type   #microsoft.graph.domainDnsCnameRecord
canonicalName domain.sharepoint.com


msoid.domain.net

Key           Value
---           -----
@odata.type   #microsoft.graph.domainDnsCnameRecord
canonicalName clientconfig.microsoftonline-p.net


enterpriseregistration.domain.net

Key           Value
---           -----
@odata.type   #microsoft.graph.domainDnsCnameRecord
canonicalName enterpriseregistration.windows.net


enterpriseenrollment.domain.net

Key           Value
---           -----
@odata.type   #microsoft.graph.domainDnsCnameRecord
canonicalName enterpriseenrollment-s.manage.microsoft.com

If the DNS record contains a name within the domain this is represented by the label. The @odata.type provides the type of DNS record expected and the value column lists the value of that record.

enterpriseenrollment.domain.net

Key           Value
---           -----
@odata.type   #microsoft.graph.domainDnsCnameRecord
canonicalName enterpriseenrollment-s.manage.microsoft.com

The previous example would be interpreted as:

DNS Record Name = enterpriseenrollment.domain.net

DNS Record Type = CNAME

DNS Record Value = enterpriseenrollment-s.manage.microsoft.com

domain.net

Key          Value
---          -----
@odata.type  #microsoft.graph.domainDnsMxRecord
mailExchange domain-net0c.mail.protection.outlook.com
preference   0

The previous example would be interpreted as:

DNS Domain = domain.net

DNS Record Type = MX

DNS Record Value = domain-net0c.mail.protection.outlook.com

DNS Record Preference = 0

In this case an MX record does not have a specific DNS host name unlike other records.

These commands should unblock scenarios where the domains blade is unavailable to you and you need to know the appropriate DNS records to create for your Office 365 integration.