Want Dynamic Groups But Don’t Want to Pay for Azure Active Directory Premium Licenses? Here’s How to Do the Job
A reader says that they like the notion of dynamic Office 365 groups, but they don’t want to pay for the Azure Active Directory premium licenses needed to use the feature. As you might recall, dynamic groups have their membership controlled by queries executed against Azure Active Directory. Every account that comes within the scope of a query must be licensed.
Instead of using dynamic groups, our reader is willing to run scheduled background jobs to populate the membership. They need some guidance as to how to approach building the script to manage membership for a group. As an example to help them, this post describes how to manage group membership based on Office location. Our first step is therefore to grab a set of mailboxes with the right value for the Office attribute and store it in a PowerShell variable. Because I’m based in Dublin, I’ll use it as the Office location.
Using the Filter parameter is important because this forces Exchange Online to process the query on the server, and we get the right results faster. That is, if your directory is correctly populated.
Adding Members to an Office 365 Group
To update the membership of an Office 365 group, we use the Add-UnifiedGroupLinks cmdlet. The simplest method is to use the variable holding the set of mailboxes as the input, passing the distinguished name of each mailbox as the link. To test everything, I created a new group called Dublin Employees and then ran this code to populate its membership.
Add-UnifiedGroupLinks -Identity DublinEmployees -LinkType Member -Links $Mbx.DistinguishedName
To check that the correct membership has been added, we can run the Get-UnifiedGroupLinks cmdlet:
Get-UnifiedGroupLinks -Identity DublinEmployees -LinkType Member
Maintaining Membership
All easy so far, but the issue is now how to maintain the membership. We don’t have to worry about deleted mailboxes as they lose membership when they are removed. But we do need to add new employees whose accounts match the filter and remove people who no longer match.
Adding new members is straightforward because we can use the same commands to form a set of matching mailboxes and run the Add-UnifiedGroupLinks command again. Exchange Online will ignore mailboxes that already exist in the membership.
Mailboxes that no longer match can be removed by running the Remove-UnifiedGroupLinks cmdlet. However, we must figure out what members to remove. One way to do this is to check the current membership against the set of mailboxes that should be members and remove any members that no longer qualify. In this code snippet, we create a hash table and populate it with the set of mailboxes (it’s faster to check against a hash table if the membership is large). We then check each of the members in the current membership against the hash table. Any member that isn’t found in the table doesn’t work in the Dublin office, so we go ahead and remove them.
# Remove lingering members from the group after they leave the Dublin office.
$Members = (Get-UnifiedGroupLinks -Identity DublinEmployees -LinkType Member | Select Alias, DistinguishedName, DisplayName)
$MembersHash = @{}
# Populate MembersHash with the current set of mailboxes from the Dublin office
ForEach ($M in $Mbx) {$MembersHash.Add($M.DistinguishedName, $M.Alias) }
# Check each member in the current membership. If they're not in the mailbox hash table, remove them from the membership
ForEach ($Member in $Members) {
If ($MembersHash.ContainsKey($Member.DistinguishedName) -eq $False) {
Write-Host "Removing" $Member.DisplayName "from Dublin Employees membership"
Remove-UnifiedGroupLinks -Identity DublinEmployees -Linktype Member -Links $Member.Alias -Confirm:$False }
}
Dealing With Group Owners
One small glitch. Group owners are also members, but a group owner might not belong to the Dublin office. The command fails if you run Remove-UnifiedGroupLinks to remove an owner from the member list without removing them as an owner first. As we don’t want to see any nasty errors, we should incorporate a check for owner status before we try to remove a member. Here’s the change I made to the script.
# Include check for group owners before we delete a member
ForEach ($Member in $Members) {
If ($MembersHash.ContainsKey($Member.DistinguishedName) -eq $False) {
If ((Get-UnifiedGroupLinks -id "Dublin Employees" -LinkType Owner | Select Alias) -Match $Member.Alias) {
Write-Host "Can't remove" $Member.DisplayName "as member - they are group owner." }
Else {
Write-Host "Removing" $Member.DisplayName "from Dublin Employees membership"
Remove-UnifiedGroupLinks -Identity DublinEmployees -Linktype Member -Links $Member.Alias -Confirm:$False }
}}
Because Teams uses Office 365 Groups to manage the membership of teams, the same approach works for Teams without the need to rewrite to use the cmdlets in the Teams PowerShell module.
More Work to Do
I’m sure other bells and whistles could be added to make the code work the way an organization wants, but that’s not the purpose of this post. I set out to answer the question and give some guidance about the commands used to maintain Office 365 group membership. There’s enough here for serious programmers (more serious than I am) to get their teeth into the problem and craft a production-quality solution.
For more information about using PowerShell
to manage Office 365 Groups (and Teams), read the comprehensive chapter in the Office 365 for IT Pros eBook.
{"id":null,"mode":"button","open_style":"in_modal","currency_code":"EUR","currency_symbol":"\u20ac","currency_type":"decimal","blank_flag_url":"https:\/\/office365itpros.com\/wp-content\/plugins\/tip-jar-wp\/\/assets\/images\/flags\/blank.gif","flag_sprite_url":"https:\/\/office365itpros.com\/wp-content\/plugins\/tip-jar-wp\/\/assets\/images\/flags\/flags.png","default_amount":100,"top_media_type":"featured_image","featured_image_url":"https:\/\/office365itpros.com\/wp-content\/uploads\/2022\/11\/cover-141x200.jpg","featured_embed":"","header_media":null,"file_download_attachment_data":null,"recurring_options_enabled":true,"recurring_options":{"never":{"selected":true,"after_output":"One time only"},"weekly":{"selected":false,"after_output":"Every week"},"monthly":{"selected":false,"after_output":"Every month"},"yearly":{"selected":false,"after_output":"Every year"}},"strings":{"current_user_email":"","current_user_name":"","link_text":"Virtual Tip Jar","complete_payment_button_error_text":"Check info and try again","payment_verb":"Pay","payment_request_label":"Office 365 for IT Pros","form_has_an_error":"Please check and fix the errors above","general_server_error":"Something isn't working right at the moment. Please try again.","form_title":"Office 365 for IT Pros","form_subtitle":null,"currency_search_text":"Country or Currency here","other_payment_option":"Other payment option","manage_payments_button_text":"Manage your payments","thank_you_message":"Thank you for supporting the work of Office 365 for IT Pros!","payment_confirmation_title":"Office 365 for IT Pros","receipt_title":"Your Receipt","print_receipt":"Print Receipt","email_receipt":"Email Receipt","email_receipt_sending":"Sending receipt...","email_receipt_success":"Email receipt successfully sent","email_receipt_failed":"Email receipt failed to send. Please try again.","receipt_payee":"Paid to","receipt_statement_descriptor":"This will show up on your statement as","receipt_date":"Date","receipt_transaction_id":"Transaction ID","receipt_transaction_amount":"Amount","refund_payer":"Refund from","login":"Log in to manage your payments","manage_payments":"Manage Payments","transactions_title":"Your Transactions","transaction_title":"Transaction Receipt","transaction_period":"Plan Period","arrangements_title":"Your Plans","arrangement_title":"Manage Plan","arrangement_details":"Plan Details","arrangement_id_title":"Plan ID","arrangement_payment_method_title":"Payment Method","arrangement_amount_title":"Plan Amount","arrangement_renewal_title":"Next renewal date","arrangement_action_cancel":"Cancel Plan","arrangement_action_cant_cancel":"Cancelling is currently not available.","arrangement_action_cancel_double":"Are you sure you'd like to cancel?","arrangement_cancelling":"Cancelling Plan...","arrangement_cancelled":"Plan Cancelled","arrangement_failed_to_cancel":"Failed to cancel plan","back_to_plans":"\u2190 Back to Plans","update_payment_method_verb":"Update","sca_auth_description":"Your have a pending renewal payment which requires authorization.","sca_auth_verb":"Authorize renewal payment","sca_authing_verb":"Authorizing payment","sca_authed_verb":"Payment successfully authorized!","sca_auth_failed":"Unable to authorize! Please try again.","login_button_text":"Log in","login_form_has_an_error":"Please check and fix the errors above","uppercase_search":"Search","lowercase_search":"search","uppercase_page":"Page","lowercase_page":"page","uppercase_items":"Items","lowercase_items":"items","uppercase_per":"Per","lowercase_per":"per","uppercase_of":"Of","lowercase_of":"of","back":"Back to plans","zip_code_placeholder":"Zip\/Postal Code","download_file_button_text":"Download File","input_field_instructions":{"tip_amount":{"placeholder_text":"How much would you like to tip?","initial":{"instruction_type":"normal","instruction_message":"How much would you like to tip? Choose any currency."},"empty":{"instruction_type":"error","instruction_message":"How much would you like to tip? Choose any currency."},"invalid_curency":{"instruction_type":"error","instruction_message":"Please choose a valid currency."}},"recurring":{"placeholder_text":"Recurring","initial":{"instruction_type":"normal","instruction_message":"How often would you like to give this?"},"success":{"instruction_type":"success","instruction_message":"How often would you like to give this?"},"empty":{"instruction_type":"error","instruction_message":"How often would you like to give this?"}},"name":{"placeholder_text":"Name on Credit Card","initial":{"instruction_type":"normal","instruction_message":"Enter the name on your card."},"success":{"instruction_type":"success","instruction_message":"Enter the name on your card."},"empty":{"instruction_type":"error","instruction_message":"Please enter the name on your card."}},"privacy_policy":{"terms_title":"Terms and conditions","terms_body":null,"terms_show_text":"View Terms","terms_hide_text":"Hide Terms","initial":{"instruction_type":"normal","instruction_message":"I agree to the terms."},"unchecked":{"instruction_type":"error","instruction_message":"Please agree to the terms."},"checked":{"instruction_type":"success","instruction_message":"I agree to the terms."}},"email":{"placeholder_text":"Your email address","initial":{"instruction_type":"normal","instruction_message":"Enter your email address"},"success":{"instruction_type":"success","instruction_message":"Enter your email address"},"blank":{"instruction_type":"error","instruction_message":"Enter your email address"},"not_an_email_address":{"instruction_type":"error","instruction_message":"Make sure you have entered a valid email address"}},"note_with_tip":{"placeholder_text":"Your note here...","initial":{"instruction_type":"normal","instruction_message":"Attach a note to your tip (optional)"},"empty":{"instruction_type":"normal","instruction_message":"Attach a note to your tip (optional)"},"not_empty_initial":{"instruction_type":"normal","instruction_message":"Attach a note to your tip (optional)"},"saving":{"instruction_type":"normal","instruction_message":"Saving note..."},"success":{"instruction_type":"success","instruction_message":"Note successfully saved!"},"error":{"instruction_type":"error","instruction_message":"Unable to save note note at this time. Please try again."}},"email_for_login_code":{"placeholder_text":"Your email address","initial":{"instruction_type":"normal","instruction_message":"Enter your email to log in."},"success":{"instruction_type":"success","instruction_message":"Enter your email to log in."},"blank":{"instruction_type":"error","instruction_message":"Enter your email to log in."},"empty":{"instruction_type":"error","instruction_message":"Enter your email to log in."}},"login_code":{"initial":{"instruction_type":"normal","instruction_message":"Check your email and enter the login code."},"success":{"instruction_type":"success","instruction_message":"Check your email and enter the login code."},"blank":{"instruction_type":"error","instruction_message":"Check your email and enter the login code."},"empty":{"instruction_type":"error","instruction_message":"Check your email and enter the login code."}},"stripe_all_in_one":{"initial":{"instruction_type":"normal","instruction_message":"Enter your credit card details here."},"empty":{"instruction_type":"error","instruction_message":"Enter your credit card details here."},"success":{"instruction_type":"normal","instruction_message":"Enter your credit card details here."},"invalid_number":{"instruction_type":"error","instruction_message":"The card number is not a valid credit card number."},"invalid_expiry_month":{"instruction_type":"error","instruction_message":"The card's expiration month is invalid."},"invalid_expiry_year":{"instruction_type":"error","instruction_message":"The card's expiration year is invalid."},"invalid_cvc":{"instruction_type":"error","instruction_message":"The card's security code is invalid."},"incorrect_number":{"instruction_type":"error","instruction_message":"The card number is incorrect."},"incomplete_number":{"instruction_type":"error","instruction_message":"The card number is incomplete."},"incomplete_cvc":{"instruction_type":"error","instruction_message":"The card's security code is incomplete."},"incomplete_expiry":{"instruction_type":"error","instruction_message":"The card's expiration date is incomplete."},"incomplete_zip":{"instruction_type":"error","instruction_message":"The card's zip code is incomplete."},"expired_card":{"instruction_type":"error","instruction_message":"The card has expired."},"incorrect_cvc":{"instruction_type":"error","instruction_message":"The card's security code is incorrect."},"incorrect_zip":{"instruction_type":"error","instruction_message":"The card's zip code failed validation."},"invalid_expiry_year_past":{"instruction_type":"error","instruction_message":"The card's expiration year is in the past"},"card_declined":{"instruction_type":"error","instruction_message":"The card was declined."},"missing":{"instruction_type":"error","instruction_message":"There is no card on a customer that is being charged."},"processing_error":{"instruction_type":"error","instruction_message":"An error occurred while processing the card."},"invalid_request_error":{"instruction_type":"error","instruction_message":"Unable to process this payment, please try again or use alternative method."},"invalid_sofort_country":{"instruction_type":"error","instruction_message":"The billing country is not accepted by SOFORT. Please try another country."}}}},"fetched_oembed_html":false}