Public/New-TcsEntraDepartmentalGroup.ps1
|
<# .SYNOPSIS Creates a departmental security group in Microsoft Entra ID. .DESCRIPTION The New-TcsEntraDepartmentalGroup function builds a group name with the same rules as Get-DepartmentalGroupName (see Get-Help Get-DepartmentalGroupName) and creates a security group with that name in Microsoft Entra ID. The group is looked up first. An existing group (same display name) is not created again: a warning is written and it is returned with Status 'Existing'. Only groups that do not exist yet are passed to -WhatIf/-Confirm and created. With -DynamicMembership the group gets the membership rule (user.department -eq "<Department>"), using the department as written (or the division when no department is given), and membershipRuleProcessingState On, so Entra ID keeps the members up to date. Dynamic membership groups need a Microsoft Entra ID P1 licence. The Microsoft.Entra.Groups New-EntraGroup (1.x) has no -MembershipRule parameter; in that case New-EntraBetaGroup from Microsoft.Entra.Beta.Groups is used, and the command stops with an error when neither supports it. With -Owner the given users or service principals are added as owners (Add-EntraGroupOwner). For every group an object is written to the pipeline with the properties Name, Id, Status (Created, Existing, Failed or WhatIf), MembershipRule and MailNickname. Requires the Microsoft Entra PowerShell module (Microsoft.Entra or Microsoft.Entra.Groups) and a session opened with Connect-Entra that can create groups, for example the Group.ReadWrite.All scope. Supports -WhatIf and -Confirm. .PARAMETER Prefix The first part of the group name, for example 'SG'. Cannot contain whitespace. Accepts pipeline input by property name. .PARAMETER Suffix An optional last part of the group name, for example 'Users'. Cannot contain whitespace. Accepts pipeline input by property name. .PARAMETER Division The division the group belongs to, for example 'Human Resources'. Accepts pipeline input by property name. .PARAMETER Department The optional department within the division, for example 'Payroll'. Accepts pipeline input by property name. With -DynamicMembership this is the value matched against user.department. .PARAMETER DynamicMembership Create a dynamic membership group with the rule (user.department -eq "<Department>"), or the division when no department is given. .PARAMETER Owner Object IDs of users or service principals to add as owners of a created group. .INPUTS System.Management.Automation.PSObject Objects with Prefix, Suffix, Division and Department properties (for example rows from Import-Csv). .OUTPUTS Tcs.Azure.DepartmentalGroup One object for each group, with Name, Id, Status, MembershipRule and MailNickname. .EXAMPLE New-TcsEntraDepartmentalGroup -Prefix 'SG' -Division 'Human Resources' -Department 'Payroll' -WhatIf Shows that the security group SG-HR-Payroll would be created. .EXAMPLE New-TcsEntraDepartmentalGroup -Prefix 'SG' -Division 'Finance' -Department 'Accounts Payable' -DynamicMembership -Owner $ownerId Creates SG-F-AccountsPayable with the rule (user.department -eq "Accounts Payable") and adds an owner. .EXAMPLE Import-Csv .\departments.csv | New-TcsEntraDepartmentalGroup -Prefix 'SG' -Suffix 'Users' Creates one group for each row; the CSV has Division and Department columns. .NOTES Author: Nigel Tatschner Company: TheCodeSaiyan .LINK Get-DepartmentalGroupName .LINK https://learn.microsoft.com/entra/identity/users/groups-dynamic-membership #> function New-TcsEntraDepartmentalGroup { [CmdletBinding(SupportsShouldProcess = $true)] [OutputType('Tcs.Azure.DepartmentalGroup')] param( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [ValidatePattern('^\S+$')] [string] $Prefix, [Parameter(ValueFromPipelineByPropertyName = $true)] [ValidatePattern('^\S*$')] [string] $Suffix, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [string] $Division, [Parameter(ValueFromPipelineByPropertyName = $true)] [string] $Department, [switch] $DynamicMembership, [ValidateNotNullOrEmpty()] [string[]] $Owner ) begin { $TelemetryArgs = @{ ModuleName = $MyInvocation.MyCommand.Module.Name ModuleVersion = [string]$MyInvocation.MyCommand.Module.Version CommandName = $MyInvocation.MyCommand.Name ExecutionID = [guid]::NewGuid().ToString() } Invoke-TelemetryCollection @TelemetryArgs -Stage Start -ClearTimer $lastError = $null $telemetrySent = $false $requiredCommands = @('Get-EntraGroup', 'New-EntraGroup') if ($Owner) { $requiredCommands += 'Add-EntraGroupOwner' } $prerequisiteError = Get-EntraPrerequisiteError -CommandName $MyInvocation.MyCommand.Name -RequiredCommand $requiredCommands # Microsoft.Entra.Groups 1.x New-EntraGroup cannot set a membership rule; the beta cmdlet can $createCommand = 'New-EntraGroup' if (-not $prerequisiteError -and $DynamicMembership) { $newGroupCommand = Get-Command -Name 'New-EntraGroup' -ErrorAction SilentlyContinue if (-not ($newGroupCommand -and $newGroupCommand.Parameters -and $newGroupCommand.Parameters.ContainsKey('MembershipRule'))) { if (Get-Command -Name 'New-EntraBetaGroup' -ErrorAction SilentlyContinue) { $createCommand = 'New-EntraBetaGroup' } else { $exception = New-Object -TypeName System.Management.Automation.CommandNotFoundException -ArgumentList ( 'Dynamic membership groups need a New-EntraGroup that supports -MembershipRule, or New-EntraBetaGroup. ' + "Install it with 'Install-Module Microsoft.Entra.Beta.Groups -Scope CurrentUser'.") $prerequisiteError = New-Object -TypeName System.Management.Automation.ErrorRecord -ArgumentList $exception, 'DynamicMembershipNotSupported', ([System.Management.Automation.ErrorCategory]::ObjectNotFound), $null } } } if ($prerequisiteError) { $telemetrySent = $true Invoke-TelemetryCollection @TelemetryArgs -Stage End -Failed $true -Exception $prerequisiteError $PSCmdlet.ThrowTerminatingError($prerequisiteError) } } process { $completed = $false try { $groupName = ConvertTo-DepartmentalGroupName -Prefix $Prefix -Division $Division -Department $Department -Suffix $Suffix if (-not $groupName) { $exception = New-Object -TypeName System.ArgumentException -ArgumentList ( "The division '$Division' contains no letters or digits that can be used in a group name.", 'Division') $errorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord -ArgumentList $exception, 'InvalidDivision', ([System.Management.Automation.ErrorCategory]::InvalidArgument), $Division $lastError = $errorRecord $PSCmdlet.WriteError($errorRecord) $completed = $true return } $divisionText = @($Division -split '\s+' | Where-Object { $_ }) -join ' ' $departmentText = @($Department -split '\s+' | Where-Object { $_ }) -join ' ' $membershipRule = $null if ($DynamicMembership) { $ruleValue = if ($departmentText) { $departmentText } else { $divisionText } # Double quotes inside a rule value are escaped with a backtick $membershipRule = '(user.department -eq "' + ($ruleValue -replace '"', '`"') + '")' } $result = [pscustomobject]@{ PSTypeName = 'Tcs.Azure.DepartmentalGroup' Name = $groupName Id = $null Status = $null MembershipRule = $membershipRule MailNickname = $null } try { # Look up first so -WhatIf and -Confirm only cover a group that would really be created. # Single quotes are doubled so the name is a valid OData string literal. $filter = "DisplayName eq '$($groupName -replace "'", "''")'" $existing = @(Get-EntraGroup -Filter $filter -ErrorAction Stop) if ($existing.Count -gt 0) { Write-Warning "Group `"$groupName`" already exists." $result.Id = $existing[0].Id $result.MailNickname = $existing[0].MailNickname $result.MembershipRule = $existing[0].MembershipRule $result.Status = 'Existing' $result $completed = $true return } $result.MailNickname = ConvertTo-TcsMailNickname -Text $groupName -HashSource $groupName if (-not $PSCmdlet.ShouldProcess($groupName, 'Create Entra ID security group')) { if ($WhatIfPreference) { $result.Status = 'WhatIf' $result } $completed = $true return } $description = if ($departmentText) { "Security group for the $departmentText department of $divisionText." } else { "Security group for $divisionText." } $groupParams = @{ DisplayName = $groupName MailNickname = $result.MailNickname Description = $description MailEnabled = $false SecurityEnabled = $true } if ($DynamicMembership) { $groupParams['GroupTypes'] = @('DynamicMembership') $groupParams['MembershipRule'] = $membershipRule $groupParams['MembershipRuleProcessingState'] = 'On' } Write-Verbose "Creating group `"$groupName`" with $createCommand" $group = & $createCommand @groupParams -ErrorAction Stop $result.Id = $group.Id $result.Status = 'Created' } catch [System.Management.Automation.PipelineStoppedException] { throw } catch { $lastError = $_ $result.Status = 'Failed' $result Write-Error -ErrorRecord $_ $completed = $true return } foreach ($ownerId in $Owner) { try { Add-EntraGroupOwner -GroupId $result.Id -OwnerId $ownerId -ErrorAction Stop } catch { $lastError = $_ Write-Error -ErrorRecord $_ } } $result $completed = $true } catch { # Reached when the caller asked for errors to stop (-ErrorAction Stop) $lastError = $_ throw } finally { # The end block does not run after a terminating error or when a downstream command # (for example Select-Object -First) stops the pipeline, so End telemetry is sent here. if (-not $completed -and -not $telemetrySent) { $telemetrySent = $true if ($lastError) { Invoke-TelemetryCollection @TelemetryArgs -Stage End -Failed $true -Exception $lastError } else { Invoke-TelemetryCollection @TelemetryArgs -Stage End } } } } end { if (-not $telemetrySent) { $telemetrySent = $true if ($lastError) { Invoke-TelemetryCollection @TelemetryArgs -Stage End -Failed $true -Exception $lastError } else { Invoke-TelemetryCollection @TelemetryArgs -Stage End } } } } |