├── .github └── workflows │ ├── Github2Gallery.yml │ └── Tests.yml ├── .vscode └── settings.json ├── 365AutomatedLab.psd1 ├── 365AutomatedLab.psm1 ├── CHANGELOG.md ├── Functions ├── Private │ └── Verify-CT365TeamsCreation.ps1 └── Public │ ├── Copy-WorksheetName.ps1 │ ├── Export-CT365ProdGroupToExcel.ps1 │ ├── Export-CT365ProdTeamstoExcel.ps1 │ ├── Export-CT365ProdUserToExcel.ps1 │ ├── New-CT365DataEnvironment.ps1 │ ├── New-CT365Group.ps1 │ ├── New-CT365GroupByUserRole.ps1 │ ├── New-CT365SharePointSite.ps1 │ ├── New-CT365Teams.ps1 │ ├── New-CT365User.ps1 │ ├── Remove-CT365AllDeletedM365Groups.ps1 │ ├── Remove-CT365AllSitesFromRecycleBin.ps1 │ ├── Remove-CT365Group.ps1 │ ├── Remove-CT365GroupByUserRole.ps1 │ ├── Remove-CT365SharePointSite.ps1 │ ├── Remove-CT365Teams.ps1 │ ├── Remove-CT365User.ps1 │ └── Set-CT365SPDistinctNumber.ps1 ├── LabSources └── 365DataEnvironment.xlsx ├── Presentations ├── 02-08-2024-PowerShellPulse.pdf └── Summary.md ├── README.md ├── Static ├── 365automatedlab.png └── 365automatedlabIcon.png └── Tests ├── New-CT365Group.Tests.ps1 ├── New-CT365GroupByUserRole.Tests.ps1 ├── New-CT365SharePointSite.Tests.ps1 ├── New-CT365Teams.Tests.ps1 ├── New-CT365User.Tests.ps1 ├── Remove-CT365AllDeletedM365Groups.Tests.ps1 ├── Remove-CT365Group.Tests.ps1 ├── Remove-CT365GroupByUserRole.Tests.ps1 ├── Remove-CT365SharePointSite.Tests.ps1 ├── Remove-CT365Teams.Tests.ps1 ├── Remove-CT365User.Tests.ps1 └── Set-CT365SPDistinctNumber.Tests.ps1 /.github/workflows/Github2Gallery.yml: -------------------------------------------------------------------------------- 1 | name: Publish to PowerShell Gallery 2 | # Controls when the workflow will run 3 | on: 4 | # Allows you to run this workflow manually from the Actions tab 5 | workflow_dispatch: 6 | 7 | jobs: 8 | publish-to-gallery: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Build and publish 13 | env: 14 | NUGET_KEY: ${{ secrets.PSGALLERYAPI }} 15 | shell: pwsh 16 | run: | 17 | Set-PSRepository psgallery -InstallationPolicy trusted 18 | Install-Module ImportExcel, ExchangeOnlineManagement, Microsoft.Graph.Users, Microsoft.Graph.Groups, Microsoft.Graph.Identity.DirectoryManagement, Microsoft.Graph.Users.Actions, PSFramework, PnP.PowerShell -confirm:$false -force 19 | Publish-Module -Path ./ -NuGetApiKey $env:NUGET_KEY -Verbose 20 | -------------------------------------------------------------------------------- /.github/workflows/Tests.yml: -------------------------------------------------------------------------------- 1 | name: Pester 2 | on: 3 | push: 4 | branches: [ main ] 5 | jobs: 6 | test-pwsh: 7 | strategy: 8 | matrix: 9 | platform: [ubuntu-latest, macos-latest, windows-latest] 10 | runs-on: ${{ matrix.platform }} 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Run Pester tests (pwsh) 14 | run: | 15 | Write-host $PSVersionTable.PSVersion.Major $PSVersionTable.PSRemotingProtocolVersion.Minor 16 | Set-PSRepository psgallery -InstallationPolicy trusted 17 | Install-Module -Name Pester -confirm:$false -Force 18 | Invoke-Pester -Path ./tests 19 | shell: pwsh 20 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /365AutomatedLab.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module '365AutomatedLab' 3 | # 4 | # Generated by: Clayton Tyger 5 | # 6 | # Generated on: 6/1/2023 7 | # 8 | 9 | @{ 10 | 11 | # Script module or binary module file associated with this manifest. 12 | RootModule = '365AutomatedLab.psm1' 13 | 14 | # Version number of this module. 15 | ModuleVersion = '2.12.0' 16 | 17 | # Supported PSEditions 18 | CompatiblePSEditions = 'Core' 19 | 20 | # ID used to uniquely identify this module 21 | GUID = 'd2cf0a82-aeab-4c4c-83fc-764cf4c23ffb' 22 | 23 | # Author of this module 24 | Author = 'Clayton Tyger' 25 | 26 | # Company or vendor of this module 27 | CompanyName = 'Clayton Tyger' 28 | 29 | # Copyright statement for this module 30 | Copyright = '(c) Clayton Tyger. All rights reserved.' 31 | 32 | # Description of the functionality provided by this module 33 | Description = 'This module will allow you to create a 365 Development Environment from an Excel workbook' 34 | 35 | # Minimum version of the PowerShell engine required by this module 36 | PowerShellVersion = '7.1' 37 | 38 | # Name of the PowerShell host required by this module 39 | # PowerShellHostName = '' 40 | 41 | # Minimum version of the PowerShell host required by this module 42 | # PowerShellHostVersion = '' 43 | 44 | # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 45 | # DotNetFrameworkVersion = '' 46 | 47 | # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 48 | # ClrVersion = '' 49 | 50 | # Processor architecture (None, X86, Amd64) required by this module 51 | # ProcessorArchitecture = '' 52 | 53 | # Modules that must be imported into the global environment prior to importing this module 54 | RequiredModules = @( 55 | @{ ModuleName='ImportExcel'; ModuleVersion='7.8.2' } 56 | @{ ModuleName='ExchangeOnlineManagement'; ModuleVersion='3.5.0' } 57 | @{ ModuleName='Microsoft.Graph.Users'; ModuleVersion='2.25.0' } 58 | @{ ModuleName='Microsoft.Graph.Groups'; ModuleVersion='2.25.0' } 59 | @{ ModuleName='Microsoft.Graph.Identity.DirectoryManagement'; ModuleVersion='2.25.0' } 60 | @{ ModuleName='Microsoft.Graph.Users.Actions'; ModuleVersion='2.25.0' } 61 | @{ ModuleName='PSFramework'; ModuleVersion='1.8.289' } 62 | @{ ModuleName='PnP.PowerShell'; ModuleVersion='2.12.0' } 63 | ) 64 | 65 | # Assemblies that must be loaded prior to importing this module 66 | # RequiredAssemblies = @() 67 | 68 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 69 | # ScriptsToProcess = @() 70 | 71 | # Type files (.ps1xml) to be loaded when importing this module 72 | # TypesToProcess = @() 73 | 74 | # Format files (.ps1xml) to be loaded when importing this module 75 | # FormatsToProcess = @() 76 | 77 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 78 | # NestedModules = @() 79 | 80 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. 81 | FunctionsToExport = @( 82 | 'New-CT365Group', 83 | 'New-CT365User', 84 | 'New-CT365GroupByUserRole', 85 | 'Remove-CT365Group', 86 | 'Remove-CT365User', 87 | 'Remove-CT365GroupByUserRole', 88 | 'Copy-WorksheetName', 89 | 'New-CT365DataEnvironment', 90 | 'Export-CT365ProdUserToExcel', 91 | 'New-CT365SharePointSite', 92 | 'New-CT365Teams', 93 | 'Remove-CT365Teams', 94 | 'Remove-CT365SharePointSite', 95 | 'Set-CT365SPDistinctNumber', 96 | 'Remove-CT365AllDeletedM365Groups', 97 | 'Export-CT365ProdGroupToExcel', 98 | 'Export-CT365ProdTeamsToExcel', 99 | 'Verify-CT365TeamsCreation', 100 | 'Remove-CT365AllSitesFromRecycleBin' 101 | 102 | ) 103 | 104 | # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. 105 | CmdletsToExport = @() 106 | 107 | # Variables to export from this module 108 | VariablesToExport = @() 109 | 110 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. 111 | AliasesToExport = @() 112 | 113 | # DSC resources to export from this module 114 | # DscResourcesToExport = @() 115 | 116 | # List of all modules packaged with this module 117 | # ModuleList = @() 118 | 119 | # List of all files packaged with this module 120 | # FileList = @() 121 | 122 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 123 | PrivateData = @{ 124 | 125 | PSData = @{ 126 | 127 | # Tags applied to this module. These help with module discovery in online galleries. 128 | Tags = @('Office365', 'Automation', 'MSGraph', 'Cloud', 'Teams', 'Configuration', 'Sharepoint', 'Excel', 'Exchange', 'MacOS', 'Windows', 'PSEdition_Core') 129 | 130 | # A URL to the license for this module. 131 | # LicenseUri = '' 132 | 133 | # A URL to the main website for this project. 134 | ProjectUri = 'https://github.com/DevClate/365AutomatedLab' 135 | 136 | # A URL to an icon representing this module. 137 | IconUri = 'https://raw.githubusercontent.com/DevClate/365AutomatedLab/78573608de64cefec833d808939fb95edaa456ad/Static/365automatedlabIcon.png' 138 | 139 | # ReleaseNotes of this module 140 | ReleaseNotes = 'https://github.com/DevClate/365AutomatedLab/blob/78573608de64cefec833d808939fb95edaa456ad/CHANGELOG.md' 141 | 142 | # Prerelease string of this module 143 | # Prerelease = '' 144 | 145 | # Flag to indicate whether the module requires explicit user acceptance for install/update/save 146 | # RequireLicenseAcceptance = $false 147 | 148 | # External dependent modules of this module 149 | # ExternalModuleDependencies = @() 150 | 151 | } # End of PSData hashtable 152 | 153 | } # End of PrivateData hashtable 154 | 155 | # HelpInfo URI of this module 156 | # HelpInfoURI = '' 157 | 158 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 159 | # DefaultCommandPrefix = '' 160 | 161 | } 162 | 163 | -------------------------------------------------------------------------------- /365AutomatedLab.psm1: -------------------------------------------------------------------------------- 1 | $FunctionFiles = $("$PSScriptRoot\Functions\Public\","$PSScriptRoot\Functions\Private\")| Get-Childitem -file -Recurse -Include "*.ps1" -ErrorAction SilentlyContinue 2 | 3 | foreach($FunctionFile in $FunctionFiles){ 4 | try { 5 | . $FunctionFile.FullName 6 | } 7 | catch { 8 | Write-Error -Message "Failed to import function: '$($FunctionFile.FullName)': $_" 9 | } 10 | } 11 | 12 | Export-ModuleMember -Function $FunctionFiles.BaseName -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 365AutomatedLab Changelog 2 | 3 | ### 2.11.0 4 | 5 | **Brief Summary** 6 | 7 | This is a quick update to remove `Connect-PnP` directly in the modules so that you can choose the way you want to connect now since their update. I’ll be adding ways to connect in the future, but want to look at the best way to give everyone options. 8 | 9 | ### New Features 10 | 11 | - Adding documentation for Connecting to PnP module now. 12 | - [PnP PowerShell Documentation](https://pnp.github.io/powershell/) 13 | 14 | ### Breaking Changes 15 | 16 | - Removed `Connect-PNP` 17 | - Removed `Connect-PNP` till I/we can setup multiple ways to offer to connect to PNP with the new mandated way. 18 | - `New-CT365SharePointSite` 19 | - `New-CT365Teams` 20 | - `Export-CT365ProdTeamstoExcel` 21 | - `Remove-CT365AllDeletedM365Groups` 22 | - `Remove-CT365AllSitesFromRecycleBin` 23 | - `Remove-CT365SharePointSite` 24 | - `Remove-CT365Teams` 25 | - Change `RequiredVersion PnP.PowerShell 2.2` to `ModuleVersion 2.2.0` 26 | - In the future, I will update to require a higher version, but do not want to break change. I have confirmed it works on 2.2.0 and 2.12. 27 | 28 | ### 2.10.2 29 | 30 | **Brief Summary** 31 | 32 | Removed required Microsoft.Identity.Client as causing issues on Mac 33 | 34 | ### 2.10.1 35 | 36 | **Brief Summary** 37 | 38 | Fixed the code that outputs into the console confirming which manager was added 39 | 40 | ### 2.10.0 41 | 42 | **Brief Summary** 43 | 44 | Added new manager and added a swtich named UseDeveloperPackE5 to add the developer licenses to all added users. 45 | 46 | **Updated Features** 47 | 48 | New-CTUser 49 | 50 | * Added Manager 51 | * Add only Dev licenses option 52 | * Updated help 53 | * Allow optional parameters to be empty 54 | 55 | Added "Discussions" for feature requests and general discussions 56 | 57 | ### 2.9.0 58 | 59 | **Brief Summary** 60 | 61 | Added new parameters to cmdlets and cleaned up some formatting. 62 | 63 | **Updated Features** 64 | 65 | *New-CT365User* 66 | 67 | * CompanyName 68 | * EmployeeHireDate 69 | * EmplyeeId 70 | * EmployeeType 71 | * Employee, Contractor, Consultant, or Vendor. 72 | * FaxNumber 73 | 74 | *New-CT365DataEnvironment* 75 | 76 | * CompanyName 77 | * EmployeeHireDate 78 | * EmplyeeId 79 | * EmployeeType 80 | * FaxNumber 81 | 82 | *Export-CT365ProdUserToExcel* 83 | 84 | * CompanyName 85 | * EmployeeHireDate 86 | * EmplyeeId 87 | * EmployeeType 88 | * FaxNumber 89 | 90 | *365DataEnvironment.xlsx* 91 | 92 | * CompanyName 93 | * EmployeeHireDate 94 | * EmplyeeId 95 | * EmployeeType 96 | * FaxNumber 97 | 98 | ### 2.8.0 99 | 100 | **Updated Feature** 101 | 102 | *New-CT365Group* 103 | 104 | * Can now add a owner to each group, if you don't it will default to User Principal Name parameter 105 | 106 | ### 2.7.0 107 | 108 | **Fixed** 109 | 110 | There was an issue using PnP.PowerShell 2.3 and anything newer than 4.50.0 for the Microsoft Identity Client module. Also Microsoft Identity Client was 4.50.0.0 and was changed back to 4.50.0. I made PnP.PowerShell 2.2 and Microsoft Identity Client 4.50.0 as the required versions for now until more testing. It does seem like a known issue with PnP.PowerShell 2.3. Please let me know if you run into any issues. 111 | 112 | ### 2.6.0 113 | 114 | **Fixed** 115 | 116 | *New-CT365DataEnvironment* 117 | 118 | * Didn't always handle creating new files correctly, and changed it so it doesn’t ask you if you want to create file, as it wasn’t consistent. Looking into adding the feature back later once it is more stable. 119 | 120 | ### 2.5.0 121 | 122 | **Updated Features** 123 | 124 | *New-CT365DataEnvironment* 125 | 126 | * Will now only allow .xlsx files, and will confirm that the path of where you want to save it is correct. 127 | 128 | *New-CT365User* 129 | 130 | * Will now only allow .xlsx files first, then check if the path is correct 131 | 132 | *New-CT365Group* 133 | 134 | * Will now only allow .xlsx files first, then check if the path is correct 135 | 136 | *New-CT365GroupByUserRole* 137 | 138 | * Will now only allow .xlsx files first, then check if the path is correct 139 | 140 | *New-CT365SharePointSite* 141 | 142 | * Will now only allow .xlsx files first, then check if the path is correct 143 | 144 | *New-CT365Teams* 145 | 146 | * Will now only allow .xlsx files first, then check if the path is correct 147 | 148 | *Remove-CT365Group* 149 | 150 | * Will now only allow .xlsx files first, then check if the path is correct 151 | 152 | *Remove-CT365GroupByUserRole* 153 | 154 | * Will now only allow .xlsx files first, then check if the path is correct 155 | 156 | *Remove-CT365SharePointSite* 157 | 158 | * Will now only allow .xlsx files first, then check if the path is correct 159 | 160 | *Remove-CT365Teams* 161 | 162 | * Will now only allow .xlsx files first, then check if the path is correct 163 | 164 | *Remove-CT365User* 165 | 166 | * Will now only allow .xlsx files first, then check if the path is correct 167 | 168 | *Export-CT365ProdUserToExcel* 169 | 170 | * Will now only allow .xlsx files first, then check if the path is correct 171 | 172 | Export-CT365ProdTeamsToExcel 173 | 174 | * Will now only allow .xlsx files first, then check if path is correct 175 | 176 | ## 2.4.0 177 | 178 | ****************New Features**************** 179 | 180 | *Remove-CT365AllSitesFromRecycleBin* 181 | 182 | * Deletes all SharePoint sites from the recycle Bin 183 | 184 | ## 2.3.0 185 | 186 | ************************New Features************************ 187 | 188 | *Export-CT365ProdUserToExcel* 189 | 190 | * Default now includes Developer License 191 | * Use -NoLicense to remove 192 | 193 | ************Export-CT365ProdTeamsToExcel************ 194 | 195 | - Exports Channel Type now 196 | - Exports Channel Descriptions now 197 | 198 | **********Fixes********** 199 | 200 | Updated function name for New-CT365DataEnvironment within ps1 file 201 | 202 | Updated headers in New-CT365DataEnvironment to include Sites and Channels description. 203 | 204 | Updated formatting of README 205 | 206 | ## 2.2.0 207 | 208 | **New Features** 209 | 210 | New-CT365Teams - added functionality to create channels and their descriptions. Currently you’ll set one owner for all Teams. Please create an issue if you would like to see the option for owners per Teams and Channels. 211 | 212 | Verify-CT365TeamsCreation - internal cmdlet to verify Teams creation 213 | 214 | **Breaking Changes** 215 | 216 | None 217 | 218 | ## 2.1.0 219 | 220 | **Fixed:** 221 | Changed function name inside code from Export-CT365GroupToExcel to Export-CT365ProdGroupToExcel. 222 | 223 | ## 2.0.0 224 | 225 | **New Features** 226 | 227 | Export-CT365Teams - This will export the teams from your production tenant to an Excel worksheet named Teams. 228 | 229 | **Breaking Changes** 230 | 231 | For the 3 functions below, there will no longer be the parameter for WorkbookName, it will only be filepath going forward. This is to keep it consistent with the other functions. If you would rather have the WorkbookName, please let me know and if there is enough interest, I'll change that to the standard. 232 | 233 | - Export-CT365ProdGroupToExcel 234 | - Export-CT365ProdUserToExcel 235 | - New-CT365DataEnvironment 236 | 237 | ## 1.1.0 238 | 239 | Export-CT365ProdUserToExcel function added to enable you to export your production groups to a template that is easily imported into your dev tenant. 240 | 241 | ## 1.0.0 242 | 243 | Fixed Issues: 244 | 245 | Remove-CT365SharePointSite now behaves correctly. If you only want to delete the sites, run Remove-CT365SharePointSite, and if you want to permanently delete them, you have to run previous command, wait till SharePoint processes(10-20 minutes), then run Remove-CT365SharePointSite -PermanentlyDelete. 246 | 247 | ## 0.1.8 248 | 249 | Added Remove-CT365AllDeletedM365Groups. This will permanently delete all deleted Modern Microsoft 365 Groups. 250 | 251 | ## 0.1.7 252 | 253 | Added Set-CT365SPDistinctNumber. Currently I have it so Sharepoint Sites have a number after them for testing so I know which ones I'm working on and not having to create "real" names for each. This allows you to easily rename the site names in one quick line. I do this as SharePoint Team sites never can fully delete fast as I want while testing. 254 | 255 | ## 0.1.6 256 | 257 | Minor formatting 258 | 259 | Confirmed working upload to PowerShell Gallery GitHub Action 260 | 261 | ## 0.1.5 262 | 263 | Added better tags and added tag to show it works on MacOS in PowerShell Gallery 264 | 265 | ## 0.1.4 266 | 267 | Confirmed working on Mac OS 268 | 269 | Added microsoft.identity.client v4.50.0.0 into required modules 270 | 271 | Added microsoft.identity.client module to import for New-CT365Teams 272 | 273 | Added microsoft.identity.client module to import for Remove-CT365Teams 274 | 275 | Fixed spelling error for UserPrincipalName on New-CT365Teams 276 | 277 | Export-CT365ProdUserToExcel now matches exactly for importing into Dev(only need to add licensing) 278 | 279 | ## 0.1.3 280 | 281 | Fixed issue with New-CT365SharePointSite not creating each of the different sites correctly every time 282 | 283 | ## 0.1.2 284 | 285 | Updated/Created Pester Tests 286 | 287 | ## 0.1.1 288 | 289 | Updated URI for Icon and Documentation 290 | -------------------------------------------------------------------------------- /Functions/Private/Verify-CT365TeamsCreation.ps1: -------------------------------------------------------------------------------- 1 | function Verify-CT365TeamsCreation { 2 | param( 3 | [string]$teamName, 4 | [int]$retryCount = 5, 5 | [int]$delayInSeconds = 10 6 | ) 7 | 8 | for ($i = 0; $i -lt $retryCount; $i++) { 9 | $existingTeam = Get-PnPTeamsTeam | Where-Object { $_.DisplayName -eq $teamName } 10 | if ($existingTeam) { 11 | return $true 12 | } 13 | Start-Sleep -Seconds $delayInSeconds 14 | } 15 | return $false 16 | } -------------------------------------------------------------------------------- /Functions/Public/Copy-WorksheetName.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | This function copies the names of all the worksheets in an Excel file and exports them into a CSV file. 4 | 5 | .DESCRIPTION 6 | The function Copy-WorksheetName takes two parameters, the file path of the Excel file and the output path of the CSV file. It reads the Excel file, extracts the names of all worksheets, and exports these names into a CSV file. 7 | 8 | .PARAMETER FilePath 9 | The path to the Excel file. This is a mandatory parameter and it accepts pipeline input. 10 | 11 | .PARAMETER outputCsvPath 12 | The path where the CSV file will be created. This is a mandatory parameter and it accepts pipeline input. 13 | 14 | .EXAMPLE 15 | Copy-WorksheetName -FilePath "C:\path\to\your\excel\file.xlsx" -outputCsvPath "C:\path\to\your\output\file.csv" 16 | 17 | This will read the Excel file located at "C:\path\to\your\excel\file.xlsx", get the names of all worksheets, and export these names to a CSV file at "C:\path\to\your\output\file.csv". 18 | 19 | .NOTES 20 | This function requires the ImportExcel module to be installed. If not already installed, you can install it by running Install-Module -Name ImportExcel. 21 | 22 | .INPUTS 23 | System.String. You can pipe a string that contains the file path to this cmdlet. 24 | 25 | .OUTPUTS 26 | System.String. This cmdlet outputs a CSV file containing the names of all worksheets in the Excel file. 27 | 28 | .LINK 29 | https://github.com/dfinke/ImportExcel 30 | #> 31 | function Copy-WorksheetName { 32 | [CmdletBinding()] 33 | param ( 34 | [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] 35 | [string]$FilePath, 36 | [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] 37 | [string]$outputCsvPath 38 | ) 39 | 40 | if (!(Test-Path $FilePath)) { 41 | Write-PSFMessage -Level Error -Message "Excel file not found at the specified path: $FilePath" -Target $FilePath 42 | return 43 | } 44 | 45 | Import-Module ImportExcel 46 | Import-Module PSFramework 47 | 48 | # Import Excel file 49 | $excel = Import-excel -ExcelPackage $FilePath 50 | 51 | '"'+((Get-ExcelFileSummary $excel).WorksheetName -join '","')+'"' | Export-Csv -Path $outputCsvPath 52 | } -------------------------------------------------------------------------------- /Functions/Public/Export-CT365ProdGroupToExcel.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Exports Office 365 group data to an Excel file. 4 | 5 | .DESCRIPTION 6 | The Export-CT365ProdGroupToExcel function connects to Microsoft Graph, retrieves Office 365 group data based on specified filters, and exports the data to an Excel file. It supports limiting the number of groups to be retrieved. 7 | 8 | .PARAMETER FilePath 9 | Specifies the path to the Excel file (.xlsx) where the group data will be exported. The directory must exist, and the file must have a .xlsx extension. 10 | 11 | .PARAMETER GroupLimit 12 | Limits the number of groups to retrieve. If set to 0 (default), there is no limit. 13 | 14 | .EXAMPLE 15 | Export-CT365ProdGroupToExcel -FilePath "C:\Groups\Groups.xlsx" 16 | 17 | Exports all Office 365 groups to the specified Excel file. 18 | 19 | .EXAMPLE 20 | Export-CT365ProdGroupToExcel -FilePath "C:\Groups\LimitedGroups.xlsx" -GroupLimit 50 21 | 22 | Exports the first 50 Office 365 groups to the specified Excel file. 23 | 24 | .NOTES 25 | Requires the Microsoft.Graph.Authentication, Microsoft.Graph.Groups, ImportExcel, and PSFramework modules. 26 | 27 | The user executing this script must have permissions to access group data via Microsoft Graph. 28 | 29 | .LINK 30 | https://docs.microsoft.com/en-us/graph/api/resources/groups-overview?view=graph-rest-1.0 31 | 32 | #> 33 | function Export-CT365ProdGroupToExcel { 34 | [CmdletBinding()] 35 | param ( 36 | [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] 37 | [ValidateScript({ 38 | $isValid = $false 39 | $extension = [System.IO.Path]::GetExtension($_) 40 | $directory = [System.IO.Path]::GetDirectoryName($_) 41 | 42 | if ($extension -ne '.xlsx') { 43 | throw "The file $_ is not an Excel file (.xlsx). Please specify a file with the .xlsx extension." 44 | } 45 | elseif (-not (Test-Path -Path $directory -PathType Container)) { 46 | throw "The directory $directory does not exist. Please specify a valid directory." 47 | } 48 | else { 49 | $isValid = $true 50 | } 51 | return $isValid 52 | })] 53 | [string]$FilePath, 54 | 55 | [Parameter()] 56 | [int]$GroupLimit = 0 57 | ) 58 | 59 | begin { 60 | Import-Module Microsoft.Graph.Authentication, Microsoft.Graph.Groups, ImportExcel, PSFramework 61 | Write-PSFMessage -Level Output -Message "Preparing to export to $FilePath" -Target $FilePath 62 | } 63 | 64 | process { 65 | # Authenticate to Microsoft Graph 66 | $Scopes = @("Group.Read.All") 67 | $Context = Get-MgContext 68 | 69 | if ([string]::IsNullOrEmpty($Context) -or ($Context.Scopes -notmatch [string]::Join('|', $Scopes))) { 70 | Connect-MGGraph -Scopes $Scopes 71 | } 72 | 73 | $groupTypeFilters = @( 74 | "groupTypes/any(c:c eq 'Unified')", 75 | "(mailEnabled eq true and securityEnabled eq false)", 76 | "(mailEnabled eq true and securityEnabled eq true)", 77 | "(mailEnabled eq false and securityEnabled eq true)" 78 | ) 79 | $filterQuery = $groupTypeFilters -join " or " 80 | 81 | $getMgGroupSplat = @{ 82 | Filter = $filterQuery 83 | Select = 'DisplayName', 'MailNickname', 'Description', 'GroupTypes', 'MailEnabled', 'SecurityEnabled' 84 | } 85 | if ($GroupLimit -gt 0) { $getMgGroupSplat.Add("Top", $GroupLimit) } 86 | else { $getMgGroupSplat.Add("All", $true) } 87 | 88 | $selectProperties = @{ 89 | Property = @( 90 | 'DisplayName', 91 | @{Name = 'PrimarySMTP'; Expression = { $_.MailNickname } }, 92 | 'Description', 93 | @{Name = 'Type'; Expression = { 94 | if ($_.GroupTypes -contains "Unified") { "365Group" } 95 | elseif ($_.MailEnabled -and -not $_.SecurityEnabled) { "365Distribution" } 96 | elseif ($_.MailEnabled -and $_.SecurityEnabled) { "365MailEnabledSecurity" } 97 | else { "365Security" } 98 | } 99 | } 100 | ) 101 | } 102 | 103 | Get-MgGroup @getMgGroupSplat | Select-Object @selectProperties | Export-Excel -Path $FilePath -WorksheetName "Groups" -AutoSize 104 | 105 | Disconnect-MgGraph -ErrorAction SilentlyContinue 106 | } 107 | 108 | end { 109 | Write-PSFMessage -Level Output -Message "Export completed. Check the file at $FilePath for the group details." 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Functions/Public/Export-CT365ProdTeamstoExcel.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Exports Microsoft Teams and their channels to an Excel file. 4 | 5 | .DESCRIPTION 6 | The Export-CT365ProdTeamsToExcel function connects to SharePoint Online and retrieves information about Microsoft Teams and their channels. It then exports this data to an Excel file. The function requires a valid SharePoint admin URL and the path to an Excel file for exporting the data. 7 | 8 | .PARAMETER FilePath 9 | Specifies the path to the Excel file (.xlsx) where the Teams and Channels data will be exported. 10 | 11 | .PARAMETER AdminUrl 12 | Specifies the SharePoint admin URL for connecting to Microsoft Teams. The URL should match the format 'tenant.sharepoint.com'. 13 | 14 | .EXAMPLE 15 | Export-CT365ProdTeamsToExcel -FilePath "C:\Teams\TeamsData.xlsx" -AdminUrl "contoso.sharepoint.com" 16 | 17 | Exports Microsoft Teams and their channels information to the specified Excel file for the given SharePoint admin URL. 18 | 19 | .NOTES 20 | Requires the PnP.PowerShell, ImportExcel, and PSFramework modules. 21 | 22 | The user executing this script must have SharePoint Online administration permissions. 23 | 24 | The function handles multiple channels per team and exports them in a structured format in the Excel file. 25 | 26 | .LINK 27 | https://docs.microsoft.com/en-us/powershell/module/sharepoint-pnp/connect-pnponline?view=sharepoint-ps 28 | 29 | #> 30 | function Export-CT365ProdTeamsToExcel { 31 | [CmdletBinding()] 32 | param ( 33 | [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] 34 | [ValidateScript({ 35 | $extension = [System.IO.Path]::GetExtension($_) 36 | $directory = [System.IO.Path]::GetDirectoryName($_) 37 | 38 | if ($extension -ne '.xlsx') { 39 | throw "The file $_ is not an Excel file (.xlsx). Please specify a file with the .xlsx extension." 40 | } 41 | if (-not (Test-Path -Path $directory -PathType Container)) { 42 | throw "The directory $directory does not exist. Please specify a valid directory." 43 | } 44 | return $true 45 | })] 46 | [string]$FilePath, 47 | 48 | [Parameter(Mandatory)] 49 | [ValidateScript({ 50 | if ($_ -match '^[a-zA-Z0-9]+\.sharepoint\.[a-zA-Z0-9]+$') { 51 | $true 52 | } 53 | else { 54 | throw "The URL $_ does not match the required format." 55 | } 56 | })] 57 | [string]$AdminUrl 58 | ) 59 | 60 | begin { 61 | Import-Module PnP.PowerShell, ImportExcel, PSFramework 62 | Write-PSFMessage -Level Host -Message "Preparing to export to $(Split-Path -Path $FilePath -Leaf)" 63 | } 64 | 65 | process { 66 | # Fetch all teams 67 | $teams = Get-PnPTeamsTeam 68 | Write-PSFMessage -Level Verbose -Message "Retrieved Microsoft Teams information" 69 | 70 | $exportData = foreach ($team in $teams) { 71 | # Fetch channels for the team, excluding 'General' 72 | $channels = Get-PnPTeamsChannel -Team $team.DisplayName | Where-Object { $_.DisplayName -ne 'General' } 73 | 74 | $teamObject = [PSCustomObject]@{ 75 | "TeamName" = $team.DisplayName 76 | "TeamDescription" = $team.Description 77 | "TeamType" = $team.Visibility 78 | } 79 | 80 | $channelCount = 1 81 | foreach ($channel in $channels) { 82 | $channelPropertyName = "Channel${channelCount}Name" 83 | $channelDescriptionPropertyName = "Channel${channelCount}Description" 84 | $channelTypePropertyName = "Channel${channelCount}Type" 85 | 86 | # Check if the channel type is 'unknownfuturevalue' and convert it to 'shared' 87 | $channelType = if ($channel.MembershipType -eq 'unknownfuturevalue') { 'shared' } else { $channel.MembershipType } 88 | 89 | $teamObject | Add-Member -NotePropertyName $channelPropertyName -NotePropertyValue $channel.DisplayName 90 | $teamObject | Add-Member -NotePropertyName $channelDescriptionPropertyName -NotePropertyValue $channel.Description 91 | $teamObject | Add-Member -NotePropertyName $channelTypePropertyName -NotePropertyValue $channelType 92 | $channelCount++ 93 | } 94 | 95 | $teamObject 96 | } 97 | 98 | # Export data to Excel 99 | $exportData | Export-Excel -Path $FilePath -WorksheetName "Teams" -AutoSize 100 | Write-PSFMessage -Level Host -Message "Data exported to Excel successfully" 101 | 102 | # Disconnect the PnP session 103 | Disconnect-PnPOnline 104 | Write-PSFMessage -Level Verbose -Message "Disconnected from Microsoft 365" 105 | 106 | } 107 | 108 | end { 109 | Write-PSFMessage -Level Host -Message "Export completed. Check the file at $FilePath for the Teams and Channels details." 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Functions/Public/Export-CT365ProdUserToExcel.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Exports Office 365 production user data to an Excel file. 4 | 5 | .DESCRIPTION 6 | The Export-CT365ProdUserToExcel function connects to Microsoft Graph, retrieves user data based on specified filters, and exports the data to an Excel file. It supports filtering by department, limiting the number of users, and an option to exclude license information. 7 | 8 | .PARAMETER FilePath 9 | Specifies the path to the Excel file (.xlsx) where the user data will be exported. The directory must exist, and the file must have a .xlsx extension. 10 | 11 | .PARAMETER DepartmentFilter 12 | Filters users by their department. If not specified, users from all departments are retrieved. 13 | 14 | .PARAMETER UserLimit 15 | Limits the number of users to retrieve. If set to 0 (default), there is no limit. 16 | 17 | .PARAMETER NoLicense 18 | If specified, the exported data will not include license information for the users. 19 | 20 | .EXAMPLE 21 | Export-CT365ProdUserToExcel -FilePath "C:\Users\Export\Users.xlsx" 22 | 23 | Exports all Office 365 production users to the specified Excel file. 24 | 25 | .EXAMPLE 26 | Export-CT365ProdUserToExcel -FilePath "C:\Users\Export\DeptUsers.xlsx" -DepartmentFilter "IT" 27 | 28 | Exports Office 365 production users from the IT department to the specified Excel file. 29 | 30 | .EXAMPLE 31 | Export-CT365ProdUserToExcel -FilePath "C:\Users\Export\Users.xlsx" -UserLimit 100 32 | 33 | Exports the first 100 Office 365 production users to the specified Excel file. 34 | 35 | .NOTES 36 | Requires the Microsoft.Graph.Authentication, Microsoft.Graph.Users, ImportExcel, and PSFramework modules. 37 | 38 | The user executing this script must have permissions to access user data via Microsoft Graph. 39 | 40 | .LINK 41 | https://docs.microsoft.com/en-us/graph/api/resources/users?view=graph-rest-1.0 42 | 43 | #> 44 | function Export-CT365ProdUserToExcel { 45 | [CmdletBinding()] 46 | param ( 47 | [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] 48 | [ValidateScript({ 49 | $extension = [System.IO.Path]::GetExtension($_) 50 | $directory = [System.IO.Path]::GetDirectoryName($_) 51 | 52 | if ($extension -ne '.xlsx') { 53 | throw "The file $_ is not an Excel file (.xlsx). Please specify a file with the .xlsx extension." 54 | } 55 | if (-not (Test-Path -Path $directory -PathType Container)) { 56 | throw "The directory $directory does not exist. Please specify a valid directory." 57 | } 58 | return $true 59 | })] 60 | [string]$FilePath, 61 | 62 | [Parameter()] 63 | [string]$DepartmentFilter, 64 | 65 | [Parameter()] 66 | [int]$UserLimit = 0, 67 | 68 | [Parameter()] 69 | [switch]$NoLicense 70 | ) 71 | 72 | begin { 73 | # Import Required Modules 74 | $ModulesToImport = "Microsoft.Graph.Authentication", "Microsoft.Graph.Users", "ImportExcel", "PSFramework" 75 | Import-Module $ModulesToImport 76 | 77 | Write-PSFMessage -Level Output -Message "Creating or Merging workbook at $FilePath" 78 | } 79 | 80 | process { 81 | # Authenticate to Microsoft Graph 82 | $Scopes = @("User.Read.All") 83 | $Context = Get-MgContext 84 | 85 | if ([string]::IsNullOrEmpty($Context) -or ($Context.Scopes -notmatch [string]::Join('|', $Scopes))) { 86 | Connect-MGGraph -Scopes $Scopes 87 | } 88 | 89 | # Build the user retrieval command 90 | $getMgUserSplat = @{ 91 | Property = 'GivenName', 'SurName', 'UserPrincipalName', 92 | 'DisplayName', 'MailNickname', 'JobTitle', 93 | 'Department', 'StreetAddress', 'City', 94 | 'State', 'PostalCode', 'Country', 95 | 'BusinessPhones', 'MobilePhone', 'FaxNumber', 'UsageLocation', 96 | 'CompanyName', 'EmployeeHireDate', 'EmployeeId', 'EmployeeType' 97 | } 98 | 99 | if (-not [string]::IsNullOrEmpty($DepartmentFilter)) { 100 | $getMgUserSplat['Filter'] = "Department eq '$DepartmentFilter'" 101 | } 102 | 103 | if ($UserLimit -gt 0) { 104 | $getMgUserSplat['Top'] = $UserLimit 105 | } 106 | else { 107 | $getMgUserSplat['All'] = $true 108 | } 109 | 110 | $selectProperties = @{ 111 | Property = @( 112 | @{Name = 'FirstName'; Expression = { $_.GivenName } }, 113 | @{Name = 'LastName'; Expression = { $_.SurName } }, 114 | @{Name = 'UserName'; Expression = { $_.UserPrincipalName -replace '@.*' } }, 115 | @{Name = 'Title'; Expression = { $_.JobTitle } }, 116 | 'Department', 'StreetAddress', 'City', 'State', 'PostalCode', 'Country', 117 | @{Name = 'PhoneNumber'; Expression = { $_.BusinessPhones } }, 118 | 'MobilePhone', 'FaxNumber', 'UsageLocation', 'CompanyName', 119 | 'EmployeeHireDate', 'EmployeeId', 'EmployeeType', 120 | @{Name = 'License'; Expression = { if ($NoLicense) { "" } else { "DEVELOPERPACK_E5" } } } 121 | ) 122 | } 123 | 124 | $userCommand = Get-MgUser @getMgUserSplat | Select-Object @selectProperties 125 | 126 | # Fetch and export users to Excel 127 | $userCommand | Export-Excel -Path $FilePath -WorksheetName "Users" -AutoSize 128 | 129 | # Disconnect from Microsoft Graph 130 | if (-not [string]::IsNullOrEmpty($(Get-MgContext))) { 131 | Disconnect-MgGraph 132 | } 133 | } 134 | 135 | end { 136 | Write-PSFMessage -Level Output -Message "Export completed. Check the file at $FilePath for the user details." 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /Functions/Public/New-CT365DataEnvironment.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Creates a new Office 365 data environment template in an Excel workbook. 4 | 5 | .DESCRIPTION 6 | The New-CT365DataEnvironment function creates a new Excel workbook with multiple worksheets 7 | for Users, Groups, Teams, Sites, and specified Job Roles. Each worksheet is formatted 8 | with predefined columns relevant to its content. 9 | 10 | .PARAMETER FilePath 11 | Specifies the path to the Excel file (.xlsx) where the data environment will be created. 12 | The function checks if the file already exists, if it's a valid .xlsx file, and if the 13 | folder path exists. 14 | 15 | .PARAMETER JobRole 16 | An array of job roles to create individual worksheets for each role in the Excel workbook. 17 | Each job role will have a worksheet with predefined columns. 18 | 19 | .EXAMPLE 20 | PS> New-CT365DataEnvironment -FilePath "C:\Data\O365Environment.xlsx" -JobRole "HR", "IT" 21 | This command creates an Excel workbook at the specified path with worksheets for Users, 22 | Groups, Teams, Sites, and additional worksheets for 'HR' and 'IT' job roles. 23 | 24 | .EXAMPLE 25 | PS> New-CT365DataEnvironment -FilePath "C:\Data\NewEnvironment.xlsx" -JobRole "Finance" 26 | This command creates an Excel workbook at the specified path with a worksheet for the 27 | 'Finance' job role, along with the standard Users, Groups, Teams, and Sites worksheets. 28 | 29 | .INPUTS 30 | None. You cannot pipe objects to New-CT365DataEnvironment. 31 | 32 | .OUTPUTS 33 | None. This function does not generate any output. 34 | 35 | .NOTES 36 | Requires the modules ImportExcel and PSFramework to be installed. 37 | 38 | .LINK 39 | https://www.powershellgallery.com/packages/ImportExcel 40 | https://www.powershellgallery.com/packages/PSFramework 41 | 42 | #> 43 | 44 | function New-CT365DataEnvironment { 45 | [CmdletBinding()] 46 | param ( 47 | [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] 48 | [ValidateScript({ 49 | if (Test-Path -Path $_ -PathType Leaf) { 50 | throw "File $_ already exists, please provide a new file path" 51 | } 52 | if (-not $_ -match '^.*\.(xlsx)$') { 53 | throw "File path $_ is not a valid .xlsx file, please provide a valid .xlsx file path" 54 | } 55 | if (-not (Test-Path -Path (Split-Path $_) -PathType Container)) { 56 | throw "Folder path for $_ does not exist, please confirm path does exist" 57 | } 58 | return $true 59 | })] 60 | [string]$FilePath, 61 | 62 | [Parameter(Mandatory)] 63 | [string[]]$JobRole 64 | ) 65 | 66 | begin { 67 | # Import Required Modules 68 | $ModulesToImport = "ImportExcel", "PSFramework" 69 | Import-Module $ModulesToImport 70 | 71 | Write-PSFMessage -Level Output -Message "Creating workbook at $FilePath" 72 | 73 | # Helper function 74 | function New-EmptyCustomObject { 75 | param ( 76 | [string[]]$PropertyNames 77 | ) 78 | 79 | $customObject = [PSCustomObject]@{} 80 | $customObject | Select-Object -Property $PropertyNames 81 | } 82 | } 83 | 84 | process { 85 | # Define properties for custom objects 86 | $propertyDefinitions = @{ 87 | Users = @( 88 | "FirstName", "LastName", "UserName", "Title", "Department", 89 | "StreetAddress", "City", "State", "PostalCode", "Country", 90 | "PhoneNumber", "MobilePhone", "FaxNumber", "UsageLocation", "CompanyName", "EmployeeHireDate", "EmployeeId", "EmployeeType", "License" 91 | ) 92 | Groups = @( 93 | "DisplayName", "PrimarySMTP", "Description", "Type" 94 | ) 95 | JobRole = @( 96 | "DisplayName", "PrimarySMTP", "Description", "Type" 97 | ) 98 | Teams = @( 99 | "TeamName", "TeamDescription", "TeamType", "Channel1Name", "Channel1Description", "Channel1Type", "Channel2Name", "Channel2Description", "Channel2Type" 100 | ) 101 | Sites = @( 102 | "Url", "Template", "TimeZone", "Title", "Alias", "SiteType" 103 | ) 104 | } 105 | 106 | # Define custom objects for each worksheet 107 | $usersObject = New-EmptyCustomObject -PropertyNames $propertyDefinitions.Users 108 | $groupsObject = New-EmptyCustomObject -PropertyNames $propertyDefinitions.Groups 109 | $teamsObject = New-EmptyCustomObject -PropertyNames $propertyDefinitions.Teams 110 | $sitesObject = New-EmptyCustomObject -PropertyNames $propertyDefinitions.Sites 111 | 112 | # Export each worksheet to the workbook 113 | $usersObject | Export-Excel -Path $FilePath -WorksheetName "Users" -ClearSheet -AutoSize 114 | $groupsObject | Export-Excel -Path $FilePath -WorksheetName "Groups" -Append -AutoSize 115 | $teamsObject | Export-Excel -Path $FilePath -WorksheetName "Teams" -Append -AutoSize 116 | $sitesObject | Export-Excel -Path $FilePath -WorksheetName "Sites" -Append -AutoSize 117 | 118 | foreach ($JobRoleItem in $JobRole) { 119 | $RoleObject = New-EmptyCustomObject -PropertyNames $propertyDefinitions.JobRole 120 | $RoleObject | Export-Excel -Path $FilePath -WorksheetName $JobRoleItem -Append -AutoSize 121 | } 122 | } 123 | 124 | end { 125 | Write-PSFMessage -Level Output -Message "Workbook created successfully at $FilePath" 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Functions/Public/New-CT365Group.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | This function creates Office 365 Groups, Distribution Groups, Mail-Enabled Security Groups, and Security Groups based on the data provided in an Excel file. 4 | .DESCRIPTION 5 | The function New-CT365Group takes the path of an Excel file, User Principal Name and a Domain as input. It creates Office 365 Groups, Distribution Groups, Mail-Enabled Security Groups, and Security Groups based on the data found in the Excel file. If a group already exists, the function will output a message and skip the creation of that group. 6 | .PARAMETER FilePath 7 | The full path to the Excel file(.xlsx) containing the data for the groups to be created. The Excel file should have a worksheet named "Groups". Each row in this worksheet should represent a group to be created. The columns in the worksheet should include the DisplayName, Type (365Group, 365Distribution, 365MailEnabledSecurity, 365Security), PrimarySMTP (without domain), and Description of the group. 8 | .PARAMETER UserPrincipalName 9 | The User Principal Name (UPN) used to connect to Exchange Online. 10 | .PARAMETER Domain 11 | The domain to be appended to the PrimarySMTP of each group to form the email address of the group. 12 | .EXAMPLE 13 | New-CT365Group -FilePath "C:\Path\to\file.xlsx" -UserPrincialName "admin@domain.com" -Domain "domain.com" 14 | This will read the Excel file "file.xlsx" located at "C:\Path\to\", use "admin@domain.com" to connect to Exchange Online, and append "@domain.com" to the PrimarySMTP of each group to form the email address of the group. 15 | .INPUTS 16 | System.String 17 | .OUTPUTS 18 | System.String 19 | The function outputs strings informing about the creation of the groups or if the groups already exist. 20 | .NOTES 21 | The function uses the ExchangeOnlineManagement and Microsoft.Graph.Groups modules to interact with Office 365. Make sure these modules are installed before running the function. 22 | .LINK 23 | Get-UnifiedGroup 24 | .LINK 25 | New-UnifiedGroup 26 | .LINK 27 | Get-DistributionGroup 28 | .LINK 29 | New-DistributionGroup 30 | .LINK 31 | Get-MgGroup 32 | .LINK 33 | New-MgGroup 34 | #> 35 | function New-CT365Group { 36 | [CmdletBinding()] 37 | param ( 38 | [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] 39 | [ValidateScript({ 40 | # First, check if the file has a valid Excel extension (.xlsx) 41 | if (-not(([System.IO.Path]::GetExtension($psitem)) -match "\.(xlsx)$")) { 42 | throw "The file path '$PSitem' does not have a valid Excel format. Please make sure to specify a valid file with a .xlsx extension and try again." 43 | } 44 | 45 | # Then, check if the file exists 46 | if (-not([System.IO.File]::Exists($psitem))) { 47 | throw "The file path '$PSitem' does not lead to an existing file. Please verify the 'FilePath' parameter and ensure that it points to a valid file (folders are not allowed)." 48 | } 49 | 50 | # Return true if both conditions are met 51 | $true 52 | })] 53 | [string]$FilePath, 54 | 55 | [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] 56 | [string]$UserPrincipalName, 57 | [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] 58 | [ValidateScript({ 59 | # Check if the domain fits the pattern 60 | switch ($psitem) { 61 | { $psitem -notmatch '^(((?!-))(xn--|_)?[a-z0-9-]{0,61}[a-z0-9]{1,1}\.)*(xn--)?[a-z]{2,}(?:\.[a-z]{2,})+$' } { 62 | throw "The provided domain is not in the correct format." 63 | } 64 | Default { 65 | $true 66 | } 67 | } 68 | })] 69 | [string]$Domain 70 | ) 71 | 72 | 73 | # Import the required modules 74 | $ModulesToImport = "ImportExcel", "Microsoft.Graph.Groups", "PSFramework", "ExchangeOnlineManagement" 75 | Import-Module $ModulesToImport 76 | 77 | # Connect to Exchange Online 78 | Connect-ExchangeOnline -UserPrincipalName $UserPrincipalName -ShowProgress $true 79 | 80 | # Connect to Microsoft Graph 81 | Connect-MgGraph -Scopes "Group.ReadWrite.All", "User.Read.All" 82 | 83 | # Import data from Excel 84 | $groups = Import-Excel -Path $FilePath -WorksheetName Groups 85 | 86 | foreach ($group in $groups) { 87 | # Append the domain to the PrimarySMTP 88 | $group.PrimarySMTP += "@$Domain" 89 | if (-not [string]::IsNullOrEmpty($group.ManagedBy)) { 90 | $group.ManagedBy += "@$Domain" 91 | } 92 | switch -Regex ($group.Type) { 93 | "^365Group$" { 94 | try { 95 | Write-PSFMessage -Level Output -Message "Creating 365 Group: $($group.DisplayName)" -Target $Group.DisplayName 96 | Get-UnifiedGroup -Identity $group.DisplayName -ErrorAction Stop 97 | Write-PSFMessage -Level Warning -Message "365 Group: $($group.DisplayName) already exists" -Target $Group.DisplayName 98 | } 99 | catch { 100 | $ManagedBy = $group.ManagedBy 101 | if ([string]::IsNullOrEmpty($ManagedBy)) { 102 | $ManagedBy = $UserPrincipalName 103 | } 104 | New-UnifiedGroup -DisplayName $group.DisplayName -PrimarySMTPAddress $group.PrimarySMTP -AccessType Private -Notes $group.Description -RequireSenderAuthenticationEnabled $False -Owner $ManagedBy 105 | Write-PSFMessage -Level Output -Message "Created 365 Group: $($Group.DisplayName) successfully" -Target $Group.DisplayName 106 | } 107 | } 108 | "^365Distribution$" { 109 | try { 110 | Write-PSFMessage -Level Output -Message "Creating 365 Distribution Group: $($group.DisplayName)" -Target $Group.DisplayName 111 | Get-DistributionGroup -Identity $group.DisplayName -ErrorAction Stop 112 | Write-PSFMessage -Level Warning -Message "365 Distribution Group $($group.DisplayName) already exists" -Target $Group.DisplayName 113 | } 114 | catch { 115 | $ManagedBy = $group.ManagedBy 116 | if ([string]::IsNullOrEmpty($ManagedBy)) { 117 | $ManagedBy = $UserPrincipalName 118 | } 119 | New-DistributionGroup -Name $group.DisplayName -DisplayName $($group.DisplayName) -PrimarySMTPAddress $group.PrimarySMTP -Description $group.Description -ManagedBy $ManagedBy -RequireSenderAuthenticationEnabled $False 120 | Write-PSFMessage -Level Output -Message "Created 365 Distribution Group: $($group.DisplayName)" -Target $Group.DisplayName 121 | } 122 | } 123 | "^365MailEnabledSecurity$" { 124 | try { 125 | Write-PSFMessage -Level Output -Message "Creating 365 Mail-Enabled Security Group: $($group.DisplayName)" -Target $Group.DisplayName 126 | Get-DistributionGroup -Identity $group.DisplayName -ErrorAction Stop 127 | Write-PSFMessage -Level Warning -Message "365 Mail-Enabled Security Group: $($group.DisplayName) already exists" -Target $Group.DisplayName 128 | } 129 | catch { 130 | $ManagedBy = $group.ManagedBy 131 | if ([string]::IsNullOrEmpty($ManagedBy)) { 132 | $ManagedBy = $UserPrincipalName 133 | } 134 | New-DistributionGroup -Name $group.DisplayName -PrimarySMTPAddress $group.PrimarySMTP -Type "Security" -Description $group.Description -ManagedBy $ManagedBy -RequireSenderAuthenticationEnabled $False 135 | Write-PSFMessage -Level Output -Message "Created 365 Mail-Enabled Security Group: $($group.DisplayName)" -Target $Group.DisplayName 136 | } 137 | } 138 | "^365Security$" { 139 | Write-PSFMessage -Level Output -Message "Creating 365 Security Group: $($group.DisplayName)" -Target $Group.DisplayName 140 | $ExistingGroup = Get-MgGroup -Filter "DisplayName eq '$($group.DisplayName)'" 141 | if ($ExistingGroup) { 142 | Write-PSFMessage -Level Warning -Message "365 Security Group: $($group.DisplayName) already exists" -Target $Group.DisplayName 143 | continue 144 | } 145 | $ManagedBy = $group.ManagedBy 146 | if ([string]::IsNullOrEmpty($ManagedBy)) { 147 | $ManagedBy = $UserPrincipalName 148 | } 149 | $GroupOwner = Get-MgUser -Filter "UserPrincipalName eq '$ManagedBy'" 150 | if ($null -eq $GroupOwner) { 151 | Write-PSFMessage -Level Error -Message "User with UserPrincipalName '$ManagedBy' not found" 152 | continue 153 | } 154 | 155 | $mailNickname = $group.PrimarySMTP.Split('@')[0] 156 | New-MgGroup -DisplayName $group.DisplayName -Description $group.Description -MailNickName $mailNickname -SecurityEnabled:$true -MailEnabled:$false 157 | $365group = Get-MgGroup -Filter "DisplayName eq '$($group.DisplayName)'" 158 | New-MgGroupOwner -GroupId $365group.Id -DirectoryObjectId $GroupOwner.Id 159 | Write-PSFMessage -Level Output -Message "Created 365 Security Group: $($group.DisplayName)" -Target $Group.DisplayName 160 | } 161 | default { 162 | Write-PSFMessage -Level Error -Message "Invalid group type for $($group.DisplayName)" -Target $Group.DisplayName 163 | } 164 | } 165 | } 166 | # Disconnect Exchange Online and Microsoft Graph sessions 167 | Disconnect-ExchangeOnline -Confirm:$false 168 | Disconnect-MgGraph 169 | } -------------------------------------------------------------------------------- /Functions/Public/New-CT365GroupByUserRole.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | This function adds a user to Microsoft 365 groups based on a provided Excel file. 4 | 5 | .DESCRIPTION 6 | The New-CT365GroupByUserRole function uses Microsoft Graph and Exchange Online Management modules to add a user to different types of Microsoft 365 groups. The group details are read from an Excel file. The group's SMTP, type, and display name are extracted from the Excel file and used to add the user to the group. 7 | 8 | .PARAMETER FilePath 9 | The path to the Excel file that contains the groups to which the user should be added. The file must contain a worksheet named as per the user role ("NY-IT" or "NY-HR"). The worksheet should contain details about the groups including the primary SMTP, group type, and display name. 10 | 11 | .PARAMETER UserEmail 12 | The email of the user to be added to the groups specified in the Excel file. 13 | 14 | .PARAMETER Domain 15 | The domain that is appended to the primary SMTP to form the group's email address. 16 | 17 | .PARAMETER UserRole 18 | The role of the user, which is also the name of the worksheet in the Excel file that contains the groups to which the user should be added. The possible values are "NY-IT" and "NY-HR". 19 | 20 | .EXAMPLE 21 | New-CT365GroupByUserRole -FilePath "C:\Users\Username\Documents\Groups.xlsx" -UserEmail "jdoe@example.com" -Domain "example.com" -UserRole "NY-IT" 22 | 23 | This command reads the groups from the "NY-IT" worksheet in the Groups.xlsx file and adds the user "jdoe@example.com" to those groups. 24 | 25 | .NOTES 26 | This function requires the ExchangeOnlineManagement, ImportExcel, PSFramwork, and Microsoft.Graph.Groups modules to be installed. It will import these modules at the start of the function. The function connects to Exchange Online and Microsoft Graph, and it will disconnect from them at the end of the function. 27 | 28 | #> 29 | function New-CT365GroupByUserRole { 30 | [CmdletBinding(SupportsShouldProcess)] 31 | param ( 32 | [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] 33 | [ValidateScript({ 34 | # First, check if the file has a valid Excel extension (.xlsx) 35 | if (-not(([System.IO.Path]::GetExtension($psitem)) -match "\.(xlsx)$")) { 36 | throw "The file path '$PSitem' does not have a valid Excel format. Please make sure to specify a valid file with a .xlsx extension and try again." 37 | } 38 | 39 | # Then, check if the file exists 40 | if (-not([System.IO.File]::Exists($psitem))) { 41 | throw "The file path '$PSitem' does not lead to an existing file. Please verify the 'FilePath' parameter and ensure that it points to a valid file (folders are not allowed)." 42 | } 43 | 44 | # Return true if both conditions are met 45 | $true 46 | })] 47 | [string]$FilePath, 48 | 49 | [Parameter(Mandatory)] 50 | [ValidateScript({ 51 | # Check if the email fits the pattern 52 | switch ($psitem) { 53 | { $psitem -notmatch "^\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$" } { 54 | throw "The provided email is not in the correct format." 55 | } 56 | Default { 57 | $true 58 | } 59 | } 60 | })] 61 | [string]$UserEmail, 62 | 63 | [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] 64 | [ValidateScript({ 65 | # Check if the domain fits the pattern 66 | switch ($psitem) { 67 | { $psitem -notmatch '^(((?!-))(xn--|_)?[a-z0-9-]{0,61}[a-z0-9]{1,1}\.)*(xn--)?[a-z]{2,}(?:\.[a-z]{2,})+$' } { 68 | throw "The provided domain is not in the correct format." 69 | } 70 | Default { 71 | $true 72 | } 73 | } 74 | })] 75 | [string]$Domain, 76 | 77 | [Parameter(Mandatory)] 78 | [string]$UserRole 79 | ) 80 | 81 | # Import Required Modules 82 | $ModulesToImport = "ImportExcel", "Microsoft.Graph.Groups", "PSFramework", "ExchangeOnlineManagement" 83 | Import-Module $ModulesToImport 84 | 85 | # Connect to Exchange Online 86 | Connect-ExchangeOnline -UserPrincipalName $UserPrincipalName -ShowProgress $true 87 | 88 | # Connect to Microsoft Graph 89 | $Scopes = @("Group.ReadWrite.All") 90 | $Context = Get-MgContext 91 | 92 | if ([string]::IsNullOrEmpty($Context) -or ($Context.Scopes -notmatch [string]::Join('|', $Scopes))) { 93 | Connect-MGGraph -Scopes $Scopes 94 | } 95 | 96 | $excelData = Import-Excel -Path $FilePath -WorksheetName $UserRole 97 | 98 | if ($PSCmdlet.ShouldProcess("Add user to groups from Excel file")) { 99 | foreach ($row in $excelData) { 100 | $GroupName = $row.PrimarySMTP += "@$domain" 101 | $GroupType = $row.Type 102 | $DisplayName = $row.DisplayName 103 | 104 | if ($PSCmdlet.ShouldProcess("Add user $UserEmail to $GroupType group $GroupName")) { 105 | try { 106 | Write-PSFMessage -Level Output -Message "Adding $UserEmail to $($GroupType):'$GroupName'" -Target $UserEmail 107 | switch -Regex ($GroupType) { 108 | "^365Group$" { 109 | Add-UnifiedGroupLinks -Identity $GroupName -LinkType Members -Links $UserEmail -ErrorAction Stop 110 | } 111 | "^(365Distribution|365MailEnabledSecurity)$" { 112 | Add-DistributionGroupMember -Identity $GroupName -Member $UserEmail -Erroraction Stop 113 | } 114 | "^365Security$" { 115 | $user = Get-MgUser -Filter "userPrincipalName eq '$UserEmail'" 116 | $ExistingGroup = Get-MgGroup -Filter "DisplayName eq '$($DisplayName)'" 117 | if ($ExistingGroup) { 118 | New-MgGroupMember -GroupId $ExistingGroup.Id -DirectoryObjectId $User.Id -ErrorAction Stop 119 | Write-PSFMessage -Level Output -Message "User $UserEmail successfully added to $GroupType group $GroupName" -Target $UserEmail 120 | } 121 | else { 122 | Write-PSFMessage -Level Warning -Message "No group found with the name: $GroupName" -Target $GroupName 123 | } 124 | 125 | } 126 | default { 127 | Write-PSFMessage -Level Warning -Message "Unknown group type: $GroupType" -Target $GroupType 128 | } 129 | } 130 | Write-PSFMessage -Level Output -Message "Added $UserEmail to $($GroupType):'$GroupName' sucessfully" -Target $UserEmail 131 | } 132 | catch { 133 | Write-PSFMessage -Level Error -Message "$($_.Exception.Message) - Error adding $UserEmail to $($GroupType):'$GroupName'" -Target $UserEmail 134 | } 135 | } 136 | } 137 | } 138 | # Disconnect Exchange Online and Microsoft Graph sessions 139 | Disconnect-ExchangeOnline -Confirm:$false 140 | if (-not [string]::IsNullOrEmpty($(Get-MgContext))) { 141 | Disconnect-MgGraph 142 | } 143 | } -------------------------------------------------------------------------------- /Functions/Public/New-CT365SharePointSite.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Creates new SharePoint Online sites based on the data from an Excel file. 4 | 5 | .DESCRIPTION 6 | The `New-365CTSharePointSite` function connects to SharePoint Online(PnP) using the provided admin URL and imports site data from the specified Excel file. It then attempts to create each site based on the data. 7 | 8 | .PARAMETER FilePath 9 | The path to the Excel file containing the SharePoint site data. The file must exist and have an .xlsx extension. 10 | 11 | .PARAMETER AdminUrl 12 | The SharePoint Online admin URL. 13 | 14 | .PARAMETER Domain 15 | The domain information required for the SharePoint site creation. 16 | 17 | .EXAMPLE 18 | New-CT365SharePointSite -FilePath "C:\path\to\file.xlsx" -AdminUrl "admin.sharepoint.com" -Domain "contoso.com" 19 | 20 | This example creates SharePoint sites using the data from the "file.xlsx" and connects to SharePoint Online using the provided admin URL. 21 | 22 | .NOTES 23 | Make sure you have the necessary modules installed: ImportExcel, PnP.PowerShell, and PSFramework. 24 | 25 | .LINK 26 | https://docs.microsoft.com/powershell/module/sharepoint-pnp/new-pnpsite 27 | #> 28 | function New-CT365SharePointSite { 29 | [CmdletBinding()] 30 | param ( 31 | [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] 32 | [ValidateScript({ 33 | # First, check if the file has a valid Excel extension (.xlsx) 34 | if (-not(([System.IO.Path]::GetExtension($psitem)) -match "\.(xlsx)$")) { 35 | throw "The file path '$PSitem' does not have a valid Excel format. Please make sure to specify a valid file with a .xlsx extension and try again." 36 | } 37 | 38 | # Then, check if the file exists 39 | if (-not([System.IO.File]::Exists($psitem))) { 40 | throw "The file path '$PSitem' does not lead to an existing file. Please verify the 'FilePath' parameter and ensure that it points to a valid file (folders are not allowed)." 41 | } 42 | 43 | # Return true if both conditions are met 44 | $true 45 | })] 46 | [string]$FilePath, 47 | 48 | [Parameter(Mandatory)] 49 | [ValidateScript({ 50 | if ($_ -match '^[a-zA-Z0-9]+\.sharepoint\.[a-zA-Z0-9]+$') { 51 | $true 52 | } 53 | else { 54 | throw "The URL $_ does not match the required format." 55 | } 56 | })] 57 | [string]$AdminUrl, 58 | 59 | 60 | [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] 61 | [ValidateScript({ 62 | # Check if the domain fits the pattern 63 | switch ($psitem) { 64 | { $psitem -notmatch '^(((?!-))(xn--|_)?[a-z0-9-]{0,61}[a-z0-9]{1,1}\.)*(xn--)?[a-z]{2,}(?:\.[a-z]{2,})+$' } { 65 | throw "The provided domain is not in the correct format." 66 | } 67 | Default { 68 | $true 69 | } 70 | } 71 | })] 72 | [string]$Domain 73 | ) 74 | begin { 75 | $PSDefaultParameterValues = @{ 76 | "Write-PSFMessage:Level" = "OutPut" 77 | "Write-PSFMessage:Target" = "Preperation" 78 | } 79 | 80 | # Import the required modules 81 | $ModulesToImport = "ImportExcel", "PnP.PowerShell", "PSFramework" 82 | Import-Module $ModulesToImport 83 | 84 | try { 85 | $SiteData = Import-Excel -Path $FilePath -WorksheetName "Sites" 86 | } 87 | catch { 88 | Write-PSFMessage -Message "Failed to import Sharepoint Site data from Excel file." -Level Error 89 | return 90 | } 91 | 92 | } 93 | 94 | process { 95 | foreach ($site in $siteData) { 96 | 97 | $siteurl = "https://$AdminUrl/sites/$($site.Url)" 98 | $PSDefaultParameterValues["Write-PSFMessage:Target"] = $site.Title 99 | Write-PSFMessage -Message "Creating Sharepoint Site: '$($site.Title)'" 100 | $newPnPSiteSplat = @{ 101 | Type = $null 102 | TimeZone = $site.Timezone 103 | Title = $site.Title 104 | ErrorAction = "Stop" 105 | } 106 | switch -Regex ($site.SiteType) { 107 | "^TeamSite$" { 108 | $newPnPSiteSplat.Type = $PSItem 109 | $newPnPSiteSplat.add("Alias", $site.Alias) 110 | 111 | } 112 | "^(CommunicationSite|TeamSiteWithoutMicrosoft365Group)$" { 113 | $newPnPSiteSplat.Type = $PSItem 114 | $newPnPSiteSplat.add("Url", $siteurl) 115 | } 116 | default { 117 | Write-PSFMessage "Unknown site type: $($site.SiteType) for site $($site.Title). Skipping." -Level Error 118 | continue 119 | } 120 | } 121 | try { 122 | New-PnPSite @newPnPSiteSplat 123 | Write-PSFMessage -Message "Created Sharepoint Site: '$($site.Title)'" 124 | } 125 | catch { 126 | Write-PSFMessage -Message "Could not create Sharepoint Site: '$($site.Title)' Skipping" -Level Error 127 | Write-PSFMessage -Message $Psitem.Exception.Message -Level Error 128 | Continue 129 | } 130 | } 131 | } 132 | 133 | end { 134 | Write-PSFMessage "SharePoint site creation process completed." 135 | # Disconnect from SharePoint Online 136 | Disconnect-PnPOnline 137 | } 138 | } -------------------------------------------------------------------------------- /Functions/Public/New-CT365Teams.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Creates new Microsoft Teams and associated channels based on data from an Excel file. 4 | 5 | .DESCRIPTION 6 | The New-CT365Teams function connects to Microsoft Teams via PnP PowerShell, reads team and channel information from an Excel file, and creates new Teams and channels as specified. It supports retry logic for team and channel creation and allows specifying a default owner. The function requires the PnP.PowerShell, ImportExcel, and PSFramework modules. 7 | 8 | .PARAMETER FilePath 9 | Specifies the path to the Excel file containing the Teams and channel data. The file must be in .xlsx format. 10 | 11 | .PARAMETER AdminUrl 12 | Specifies the SharePoint admin URL for the tenant. The URL must match the format 'tenant.sharepoint.com'. 13 | 14 | .PARAMETER DefaultOwnerUPN 15 | Specifies the default owner's User Principal Name (UPN) for the Teams and channels. 16 | 17 | .EXAMPLE 18 | PS> New-CT365Teams -FilePath "C:\TeamsData.xlsx" -AdminUrl "contoso.sharepoint.com" -DefaultOwnerUPN "admin@contoso.com" 19 | 20 | This example creates Teams and channels based on the data in 'C:\TeamsData.xlsx', using 'admin@contoso.com' as the default owner if none is specified in the Excel file. 21 | 22 | .NOTES 23 | - Requires the PnP.PowerShell, ImportExcel, and PSFramework modules. 24 | - The Excel file should have a worksheet named 'teams' with appropriate columns for team and channel data. 25 | - The function includes error handling and logging using PSFramework. 26 | 27 | #> 28 | function New-CT365Teams { 29 | [CmdletBinding()] 30 | param( 31 | [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] 32 | [ValidateScript({ 33 | # First, check if the file has a valid Excel extension (.xlsx) 34 | if (-not(([System.IO.Path]::GetExtension($psitem)) -match "\.(xlsx)$")) { 35 | throw "The file path '$PSitem' does not have a valid Excel format. Please make sure to specify a valid file with a .xlsx extension and try again." 36 | } 37 | 38 | # Then, check if the file exists 39 | if (-not([System.IO.File]::Exists($psitem))) { 40 | throw "The file path '$PSitem' does not lead to an existing file. Please verify the 'FilePath' parameter and ensure that it points to a valid file (folders are not allowed)." 41 | } 42 | 43 | # Return true if both conditions are met 44 | $true 45 | })] 46 | [string]$FilePath, 47 | 48 | [Parameter(Mandatory)] 49 | [ValidateScript({ 50 | if ($_ -match '^[a-zA-Z0-9]+\.sharepoint\.[a-zA-Z0-9]+$') { 51 | $true 52 | } 53 | else { 54 | throw "The URL $_ does not match the required format." 55 | } 56 | })] 57 | [string]$AdminUrl, 58 | 59 | 60 | [Parameter(Mandatory)] 61 | [string]$DefaultOwnerUPN 62 | ) 63 | 64 | # Check and import required modules 65 | $requiredModules = @('PnP.PowerShell', 'ImportExcel', 'PSFramework') 66 | foreach ($module in $requiredModules) { 67 | try { 68 | if (-not (Get-Module -ListAvailable -Name $module)) { 69 | throw "Module $module is not installed." 70 | } 71 | Import-Module $module 72 | } 73 | catch { 74 | Write-PSFMessage -Level Warning -Message "[$(Get-Date -Format 'u')] $_.Exception.Message" 75 | return 76 | } 77 | } 78 | 79 | try { 80 | $teamsData = Import-Excel -Path $FilePath -WorksheetName "teams" 81 | $existingTeams = Get-PnPTeamsTeam 82 | } 83 | catch { 84 | Write-PSFMessage -Level Error -Message "[$(Get-Date -Format 'u')] Failed to import data from Excel or retrieve existing teams: $($_.Exception.Message)" 85 | return 86 | } 87 | 88 | foreach ($teamRow in $teamsData) { 89 | try { 90 | $teamOwnerUPN = if ($teamRow.TeamOwnerUPN) { $teamRow.TeamOwnerUPN } else { $DefaultOwnerUPN } 91 | $existingTeam = $existingTeams | Where-Object { $_.DisplayName -eq $teamRow.TeamName } 92 | 93 | if ($existingTeam) { 94 | Write-PSFMessage -Level Host -Message "[$(Get-Date -Format 'u')] Team $($teamRow.TeamName) already exists. Skipping creation." 95 | continue 96 | } 97 | 98 | $retryCount = 0 99 | $teamCreationSuccess = $false 100 | do { 101 | try { 102 | $teamId = New-PnPTeamsTeam -DisplayName $teamRow.TeamName -Description $teamRow.TeamDescription -Visibility $teamRow.TeamType -Owners $teamOwnerUPN 103 | if (Verify-CT365TeamsCreation -teamName $teamRow.TeamName) { 104 | Write-PSFMessage -Level Host -Message "[$(Get-Date -Format 'u')] Verified creation of Team: $($teamRow.TeamName)" 105 | $teamCreationSuccess = $true 106 | break 107 | } 108 | else { 109 | Write-PSFMessage -Level Warning -Message "[$(Get-Date -Format 'u')] Team $($teamRow.TeamName) creation reported but not verified. Retrying..." 110 | } 111 | } 112 | catch { 113 | Write-PSFMessage -Level Warning -Message "[$(Get-Date -Format 'u')] Attempt $retryCount to create team $($teamRow.TeamName) failed: $($_.Exception.Message)" 114 | } 115 | $retryCount++ 116 | Start-Sleep -Seconds 5 117 | } while ($retryCount -lt 5) 118 | 119 | if (-not $teamCreationSuccess) { 120 | Write-PSFMessage -Level Error -Message "[$(Get-Date -Format 'u')] Failed to create and verify Team: $($teamRow.TeamName) after multiple retries." 121 | continue 122 | } 123 | 124 | for ($i = 1; $i -le 4; $i++) { 125 | $channelName = $teamRow."Channel${i}Name" 126 | $channelType = $teamRow."Channel${i}Type" 127 | $channelDescription = $teamRow."Channel${i}Description" 128 | $channelOwnerUPN = if ($teamRow."Channel${i}OwnerUPN") { $teamRow."Channel${i}OwnerUPN" } else { $DefaultOwnerUPN } 129 | 130 | if ($channelName -and $channelType) { 131 | $retryCount = 1 132 | $channelCreationSuccess = $false 133 | do { 134 | try { 135 | Add-PnPTeamsChannel -Team $teamId -DisplayName $channelName -Description $channelDescription -ChannelType $channelType -OwnerUPN $channelOwnerUPN 136 | Write-PSFMessage -Level Host -Message "[$(Get-Date -Format 'u')] Created Channel: $channelName in Team: $($teamRow.TeamName) with Type: $channelType and Description: $channelDescription" 137 | $channelCreationSuccess = $true 138 | break 139 | } 140 | catch { 141 | Write-PSFMessage -Level Warning -Message "[$(Get-Date -Format 'u')] Attempt $retryCount to create channel $channelName in Team: $($teamRow.TeamName) failed: $($_.Exception.Message)" 142 | $retryCount++ 143 | Start-Sleep -Seconds 10 144 | } 145 | } while ($retryCount -lt 5) 146 | 147 | if (-not $channelCreationSuccess) { 148 | Write-PSFMessage -Level Error -Message "[$(Get-Date -Format 'u')] Failed to create Channel: $channelName in Team: $($teamRow.TeamName) after multiple retries." 149 | } 150 | } 151 | } 152 | } 153 | catch { 154 | Write-PSFMessage -Level Error -Message "[$(Get-Date -Format 'u')] Error processing team $($teamRow.TeamName): $($_.Exception.Message)" 155 | } 156 | } 157 | 158 | try { 159 | Disconnect-PnPOnline 160 | } 161 | catch { 162 | Write-PSFMessage -Level Error -Message "[$(Get-Date -Format 'u')] Error disconnecting PnP Online: $($_.Exception.Message)" 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /Functions/Public/New-CT365User.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Creates a new user in Office 365. 4 | 5 | .DESCRIPTION 6 | The New-CT365User function creates a new user in Office 365 using the Microsoft Graph API. 7 | It imports user data from an Excel file and assigns licenses based on the user data. 8 | If the UseDeveloperPackE5 switch is set, it assigns the DEVELOPERPACK_E5 license to all users. 9 | 10 | .PARAMETER FilePath 11 | The path to the Excel file that contains the user data. This parameter is mandatory. 12 | 13 | .PARAMETER Domain 14 | The domain for the new users. This parameter is mandatory. 15 | 16 | .PARAMETER UseDeveloperPackE5 17 | A switch that, if set, assigns the DEVELOPERPACK_E5 license to all users. 18 | 19 | .PARAMETER Password 20 | The password for the new users. If not provided, the function will prompt for the password. 21 | 22 | .EXAMPLE 23 | New-CT365User -FilePath "C:\Users\admin\Documents\user_data.xlsx" -Domain "contoso.com" -UseDeveloperPackE5 24 | 25 | This command creates new users in Office 365 using the user data in the "user_data.xlsx" file, assigns the DEVELOPERPACK_E5 license to all users, and prompts for the password. 26 | 27 | .EXAMPLE 28 | New-CT365User -FilePath "C:\Users\admin\Documents\user_data.xlsx" -Domain "contoso.com" 29 | 30 | This command creates new users in Office 365 using the user data in the "user_data.xlsx" file, assigns named license from excel worksheet, and prompts for the password. 31 | 32 | .NOTES 33 | You need to have the necessary permissions to create users and assign licenses in Office 365. 34 | #> 35 | function New-CT365User { 36 | [CmdletBinding()] 37 | param ( 38 | [Parameter(Mandatory, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] 39 | [string]$FilePath, 40 | 41 | [Parameter(Mandatory, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] 42 | [string]$Domain, 43 | 44 | [Parameter()] 45 | [switch]$UseDeveloperPackE5, 46 | 47 | [Parameter(ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] 48 | [Security.SecureString]$Password = $(Read-Host -Prompt "Enter the password" -AsSecureString) 49 | ) 50 | 51 | # Import Required Modules 52 | $ModulesToImport = "ImportExcel", "Microsoft.Graph.Users", "Microsoft.Graph.Groups", "Microsoft.Graph.Identity.DirectoryManagement", "Microsoft.Graph.Users.Actions", "PSFramework" 53 | Import-Module $ModulesToImport 54 | 55 | # Scopes 56 | $Scopes = @("Directory.ReadWrite.All") 57 | $Context = Get-MgContext 58 | 59 | if ([string]::IsNullOrEmpty($Context) -or ($Context.Scopes -notmatch [string]::Join('|', $Scopes))) { 60 | Connect-MGGraph -Scopes $Scopes 61 | } 62 | 63 | # Import user data from Excel file 64 | $userData = $null 65 | try { 66 | $userData = Import-Excel -Path $FilePath -WorksheetName Users 67 | } 68 | catch { 69 | Write-PSFMessage -Level Error -Message "Failed to import user data from Excel file." 70 | return 71 | } 72 | 73 | foreach ($user in $userData) { 74 | # Prepare user parameters for creation 75 | $userParams = @{ 76 | UserPrincipalName = "$($user.UserName)@$Domain" 77 | GivenName = $user.FirstName 78 | Surname = $user.LastName 79 | DisplayName = "$($user.FirstName) $($user.LastName)" 80 | MailNickname = $user.UserName 81 | JobTitle = $user.Title 82 | Department = $user.Department 83 | StreetAddress = $user.StreetAddress 84 | City = $user.City 85 | State = $user.State 86 | PostalCode = $user.PostalCode 87 | Country = $user.Country 88 | UsageLocation = $user.UsageLocation 89 | CompanyName = $user.CompanyName 90 | AccountEnabled = $true 91 | PasswordProfile = @{ 92 | ForceChangePasswordNextSignIn = $false 93 | Password = $Password | ConvertFrom-SecureString -AsPlainText 94 | } 95 | } 96 | 97 | # Add optional properties if they exist 98 | foreach ($prop in @('MobilePhone', 'FaxNumber', 'EmployeeHireDate', 'EmployeeId', 'EmployeeType')) { 99 | if (-not [string]::IsNullOrEmpty($user.$prop)) { 100 | $userParams[$prop] = $user.$prop 101 | } 102 | } 103 | 104 | if (-not [string]::IsNullOrEmpty($user.PhoneNumber)) { 105 | $UserParams.BusinessPhones = @($user.PhoneNumber) 106 | } 107 | 108 | # Create the new user 109 | $createdUser = New-MgUser @userParams 110 | if ($null -ne $createdUser) { 111 | Write-PSFMessage -Level Host -Message "User created: $($userParams.UserPrincipalName)" -Target $user.UserName 112 | } else { 113 | Write-PSFMessage -Level Warning -Message "Failed to create user: $($userParams.UserPrincipalName)" -Target $user.UserName 114 | continue 115 | } 116 | 117 | # License assignment logic 118 | $licenseType = $UseDeveloperPackE5 ? 'DEVELOPERPACK_E5' : $user.License 119 | $licenses = Get-MgSubscribedSku | Where-Object { $_.SkuPartNumber -eq $licenseType } 120 | 121 | if ($licenses) { 122 | Set-MgUserLicense -UserId $createdUser.Id -AddLicenses @{ SkuId = $licenses.SkuId } -RemoveLicenses @() 123 | Write-PSFMessage -Level Host -Message "License assigned: $($licenses.SkuPartNumber) to user: $($userParams.UserPrincipalName)" -Target $user.UserName 124 | } else { 125 | Write-PSFMessage -Level Warning -Message "Failed to assign license: $licenseType to user: $($userParams.UserPrincipalName)" -Target $user.UserName 126 | } 127 | 128 | # Manager assignment if applicable 129 | if ($null -ne $user.ManagerUPN) { 130 | $managerUPNData = "$($user.ManagerUPN)@$Domain" 131 | $manager = @{ "@odata.id" = "https://graph.microsoft.com/v1.0/users/$($managerUPNData)" } 132 | 133 | $assigningManagerMessage = "Assigning manager $($managerUPNData) to user: '$($UserParams.UserPrincipalName)'" 134 | Write-PSFMessage -Level Host -Message $assigningManagerMessage -Target $user.UserName 135 | 136 | Set-MgUserManagerByRef -UserId $createdUser.Id -BodyParameter $manager 137 | 138 | # Confirm manager assignment 139 | $managerDirectoryObject = Get-MgUserManager -UserId $createdUser.Id 140 | if ($null -ne $managerDirectoryObject) { 141 | $managerUser = Get-MgUser -UserId $managerDirectoryObject.Id 142 | 143 | $assignedManagerMessage = "Assigned manager $($managerUser.UserPrincipalName) to user: '$($UserParams.UserPrincipalName)'" 144 | Write-PSFMessage -Level Host -Message $assignedManagerMessage -Target $user.UserName 145 | } 146 | else { 147 | $failedToAssignManagerMessage = "Failed to assign manager $($user.Manager) to user: '$($UserParams.UserPrincipalName)'" 148 | Write-PSFMessage -Level Warning -Message $failedToAssignManagerMessage -Target $user.UserName 149 | } 150 | } 151 | } 152 | 153 | # Close the Microsoft Graph connection 154 | Disconnect-MgGraph 155 | } 156 | -------------------------------------------------------------------------------- /Functions/Public/Remove-CT365AllDeletedM365Groups.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Removes all deleted Microsoft 365 groups from the recycle bin. 4 | 5 | .DESCRIPTION 6 | The Remove-CT365AllDeletedM365Groups function connects to a Microsoft 365 tenant and removes all Microsoft 365 groups that have been deleted and are currently in the recycle bin. It requires the PnP.PowerShell module and uses the Connect-PnPOnline cmdlet to establish the connection. 7 | 8 | .PARAMETER AdminUrl 9 | The URL of the Microsoft 365 admin center. This parameter is mandatory and specifies the tenant to connect to. 10 | 11 | .EXAMPLE 12 | PS C:\> Remove-CT365AllDeletedM365Groups -AdminUrl "contoso-admin.sharepoint.com" 13 | 14 | This example connects to the Microsoft 365 tenant at contoso-admin.sharepoint.com and removes all deleted Microsoft 365 groups. 15 | 16 | .INPUTS 17 | None. You cannot pipe objects to Remove-CT365AllDeletedM365Groups. 18 | 19 | .OUTPUTS 20 | String. The function outputs messages indicating the status of deletion operations and any errors encountered. 21 | 22 | .NOTES 23 | This function requires the PnP.PowerShell module and PSFramework 24 | 25 | #> 26 | function Remove-CT365AllDeletedM365Groups { 27 | [CmdletBinding()] 28 | Param ( 29 | [Parameter(Mandatory)] 30 | [ValidateScript({ 31 | if ($_ -match '^[a-zA-Z0-9]+\.sharepoint\.[a-zA-Z0-9]+$') { 32 | $true 33 | } 34 | else { 35 | throw "The URL $_ does not match the required format." 36 | } 37 | })] 38 | [string]$AdminUrl 39 | 40 | ) 41 | 42 | Begin { 43 | 44 | foreach ($module in @('PSFramework', 'PnP.PowerShell')) { 45 | if (-not (Get-Module -ListAvailable -Name $module)) { 46 | Install-Module $module -Scope CurrentUser 47 | } 48 | } 49 | 50 | 51 | } 52 | 53 | Process { 54 | try { 55 | 56 | $deletedGroups = Get-PnPDeletedMicrosoft365Group 57 | 58 | foreach ($group in $deletedGroups) { 59 | Remove-PnPDeletedMicrosoft365Group -Identity $group.Id 60 | Write-PSFMessage -Message "Deleted group removed: $($group.DisplayName)" -Level Host 61 | } 62 | 63 | if ($deletedGroups.Count -eq 0) { 64 | Write-PSFMessage -Message "No deleted groups found." -Level Host 65 | } 66 | else { 67 | Write-PSFMessage -Message "All deleted groups have been removed." -Level Host 68 | } 69 | } 70 | catch { 71 | Write-PSFMessage -Message "An error occurred: $_" -Level Error 72 | } 73 | } 74 | 75 | End { 76 | Disconnect-PnPOnline -ErrorAction SilentlyContinue 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Functions/Public/Remove-CT365AllSitesFromRecycleBin.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Removes all sites from the SharePoint Online recycle bin. 4 | 5 | .DESCRIPTION 6 | The Remove-CT365AllSitesFromRecycleBin function connects to SharePoint Online using the provided admin URL and then removes all sites from the SharePoint Online recycle bin. This function requires the PSFramework and PnP.PowerShell modules. 7 | 8 | .PARAMETER AdminUrl 9 | Specifies the URL of the SharePoint Online admin center. The URL must follow the format 'tenantname.sharepoint.com'. 10 | 11 | .EXAMPLE 12 | Remove-CT365AllSitesFromRecycleBin -AdminUrl "contoso-admin.sharepoint.com" 13 | 14 | Connects to the SharePoint Online admin center at 'https://contoso-admin.sharepoint.com' and removes all sites from the recycle bin. 15 | 16 | .INPUTS 17 | None 18 | 19 | .OUTPUTS 20 | None 21 | 22 | .NOTES 23 | Please add any suggestions or issues to GitHub Issues 24 | 25 | #> 26 | function Remove-CT365AllSitesFromRecycleBin { 27 | [CmdletBinding()] 28 | Param ( 29 | [Parameter(Mandatory)] 30 | [ValidateScript({ 31 | if ($_ -match '^[a-zA-Z0-9]+\.sharepoint\.[a-zA-Z0-9]+$') { 32 | $true 33 | } 34 | else { 35 | throw "The URL $_ does not match the required format." 36 | } 37 | })] 38 | [string]$AdminUrl 39 | ) 40 | 41 | Begin { 42 | # Check if required modules are available, otherwise install them 43 | foreach ($module in @('PSFramework', 'PnP.PowerShell')) { 44 | if (-not (Get-Module -ListAvailable -Name $module)) { 45 | Install-Module $module -Scope CurrentUser 46 | } 47 | } 48 | } 49 | 50 | Process { 51 | try { 52 | # Retrieve sites from recycle bin 53 | $recycleBinItems = Get-PnPTenantRecycleBinItem 54 | 55 | # Delete sites from recycle bin 56 | foreach ($item in $recycleBinItems) { 57 | Remove-PnPTenantDeletedSite -Identity $item.Url -Force 58 | Write-PSFMessage -Message "Site removed from recycle bin: $($item.Url)" -Level Host 59 | } 60 | 61 | if ($recycleBinItems.Count -eq 0) { 62 | Write-PSFMessage -Message "No sites found in the recycle bin." -Level Host 63 | } 64 | else { 65 | Write-PSFMessage -Message "All sites have been removed from the recycle bin." -Level Host 66 | } 67 | } 68 | catch { 69 | Write-PSFMessage -Message "An error occurred: $_" -Level Error 70 | } 71 | } 72 | 73 | End { 74 | Disconnect-PnPOnline -ErrorAction SilentlyContinue 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Functions/Public/Remove-CT365Group.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | This function removes Office 365 groups based on information provided in an Excel file. 4 | .DESCRIPTION 5 | The Remove-CT365Group function is used to remove Office 365 groups. The function imports data from an Excel file and uses it to remove the Office 365 groups. The Excel file should contain a list of groups with their display names and types. 6 | The function supports four types of groups: 7 | - 365Group 8 | - 365Distribution 9 | - 365MailEnabledSecurity 10 | - 365Security 11 | .PARAMETER FilePath 12 | The full path to the Excel file that contains information about the groups that should be removed. The file should contain a worksheet named 'Groups'. The 'Groups' worksheet should contain the display names and types of the groups. 13 | .PARAMETER UserPrincipalName 14 | The User Principal Name (UPN) of the account to connect to Exchange Online and Microsoft Graph. 15 | .EXAMPLE 16 | Remove-CT365Group -FilePath "C:\Path\to\file.xlsx" -UserPrincipalName "admin@contoso.com" 17 | This example removes the Office 365 groups listed in the 'Groups' worksheet of the 'file.xlsx' file, using the 'admin@contoso.com' UPN to connect to Exchange Online and Microsoft Graph. 18 | .NOTES 19 | This function requires modules ExchangeOnlineManagement, Microsoft.Graph.Groups, Microsoft.Graph.Users, PSFramework, and ImportExcel. 20 | #> 21 | function Remove-CT365Group { 22 | [CmdletBinding()] 23 | param ( 24 | [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] 25 | [ValidateScript({ 26 | # First, check if the file has a valid Excel extension (.xlsx) 27 | if (-not(([System.IO.Path]::GetExtension($psitem)) -match "\.(xlsx)$")) { 28 | throw "The file path '$PSitem' does not have a valid Excel format. Please make sure to specify a valid file with a .xlsx extension and try again." 29 | } 30 | 31 | # Then, check if the file exists 32 | if (-not([System.IO.File]::Exists($psitem))) { 33 | throw "The file path '$PSitem' does not lead to an existing file. Please verify the 'FilePath' parameter and ensure that it points to a valid file (folders are not allowed)." 34 | } 35 | 36 | # Return true if both conditions are met 37 | $true 38 | })] 39 | [string]$FilePath, 40 | 41 | [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] 42 | [string]$UserPrincipalName 43 | ) 44 | 45 | # Import the required modules 46 | $ModulesToImport = "ImportExcel", "Microsoft.Graph.Groups", "PSFramework", "ExchangeOnlineManagement", "Microsoft.Graph.Users" 47 | Import-Module $ModulesToImport 48 | 49 | # Connect to Exchange Online 50 | Connect-ExchangeOnline -UserPrincipalName $UserPrincipalName -ShowProgress $true 51 | 52 | # Connect to Microsoft Graph 53 | Connect-MgGraph -Scopes "Group.ReadWrite.All" 54 | 55 | # Import data from Excel 56 | $Groups = Import-Excel -Path $FilePath -WorksheetName Groups 57 | foreach ($Group in $Groups) { 58 | switch ($Group.Type) { 59 | "365Group" { 60 | try { 61 | Write-PSFMessage -Level Output -Message "Removing 365 Group: $($Group.DisplayName)" -Target $Group.DisplayName 62 | Get-UnifiedGroup -Identity $Group.DisplayName -ErrorAction Stop 63 | Remove-UnifiedGroup -Identity $Group.DisplayName -Confirm:$false 64 | Write-PSFMessage -Level Output -Message "Removed 365 Group: $($Group.DisplayName)" -Target $Group.DisplayName 65 | } 66 | catch { 67 | Write-PSFMessage -Level Warning -Message "365 Group $($Group.DisplayName) does not exist" -Target $Group.DisplayName -ErrorRecord $_ 68 | Continue 69 | } 70 | } 71 | "365Distribution" { 72 | try { 73 | Write-PSFMessage -Level Output "Removing 365 Distribution Group: $($Group.DisplayName)" -Target $Group.DisplayName 74 | Get-DistributionGroup -Identity $Group.DisplayName -ErrorAction Stop 75 | Remove-DistributionGroup -Identity $Group.DisplayName -Confirm:$false 76 | Write-PSFMessage -Level Output -Message "Removed Distribution Group: $($Group.DisplayName)" -Target $Group.DisplayName 77 | } 78 | catch { 79 | Write-PSFMessage -Level Warning -Message "Distribution Group $($Group.DisplayName) does not exist" -Target $Group.DisplayName -ErrorRecord $_ 80 | Continue 81 | } 82 | } 83 | "365MailEnabledSecurity" { 84 | try { 85 | Write-PSFMessage -Level Output -Message "Removing 365 Mail-Enabled Security Group: $($Group.DisplayName)" -Target $Group.DisplayName 86 | Get-DistributionGroup -Identity $Group.DisplayName -ErrorAction Stop 87 | Remove-DistributionGroup -Identity $Group.DisplayName -Confirm:$false 88 | Write-PSFMessage -Level Output -Message "Removed Mail-Enabled Security Group $($Group.DisplayName)" -Target $Group.DisplayName 89 | } 90 | catch { 91 | Write-PSFMessage -Level Warning -Message "Mail-Enabled Security Group $($Group.DisplayName) does not exist" -Target $Group.DisplayName -ErrorRecord $_ 92 | Continue 93 | } 94 | } 95 | "365Security" { 96 | Write-PSFMessage -Level Output -Message "Removing 365 Security Group $($Group.DisplayName)" -Target $Group.DisplayName 97 | $existingGroup = Get-MgGroup -Filter "DisplayName eq '$($Group.DisplayName)'" 98 | if ($existingGroup) { 99 | Remove-MgGroup -GroupId $existingGroup.Id -Confirm:$false 100 | Write-PSFMessage -Level Output -Message "Removed Security Group $($Group.DisplayName)" -Target $Group.DisplayName 101 | } 102 | else { 103 | Write-PSFMessage -Level Warning -Message "Security Group $($Group.DisplayName) does not exist" -Target $Group.DisplayName 104 | } 105 | } 106 | default { 107 | Write-PSFMessage -Level Warning -Message "Invalid group type for $($Group.DisplayName)" -Target $Group.DisplayName 108 | } 109 | } 110 | } 111 | 112 | # Disconnect Exchange Online and Microsoft Graph sessions 113 | Disconnect-ExchangeOnline -Confirm:$false 114 | Disconnect-MgGraph 115 | } -------------------------------------------------------------------------------- /Functions/Public/Remove-CT365GroupByUserRole.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Removes a user from Office 365 groups as specified in an Excel file. 4 | 5 | .DESCRIPTION 6 | This function removes a user from different types of Office 365 groups as specified in an Excel file. 7 | The function uses the ExchangeOnlineManagement, ImportExcel, Microsoft.Graph.Groups, and Microsoft.Graph.Users modules. 8 | 9 | The function first connects to Exchange Online and Microsoft Graph using the UserPrincipalName provided. 10 | It then imports data from an Excel file and iterates through each row. For each row, it removes the user from the group based on the group type. 11 | 12 | .PARAMETER FilePath 13 | This mandatory parameter specifies the path to the Excel file which contains information about the groups from which the user should be removed. 14 | 15 | .PARAMETER UserEmail 16 | This mandatory parameter specifies the email of the user who should be removed from the groups. 17 | 18 | .PARAMETER Domain 19 | This mandatory parameter specifies the domain of the user. The domain is used to construct the group name. 20 | 21 | .PARAMETER UserRole 22 | This mandatory parameter specifies the user's role. It should be either "NY-IT" or "NY-HR". This parameter is used to identify the worksheet in the Excel file to import. 23 | 24 | .EXAMPLE 25 | Remove-CT365GroupByUserRole -FilePath "C:\Path\to\file.xlsx" -UserEmail "johndoe@example.com" -Domain "example.com" -UserRole "NY-IT" 26 | This example removes the user "johndoe@example.com" from the groups specified in the "NY-IT" worksheet of the Excel file at "C:\Path\to\file.xlsx". 27 | 28 | .NOTES 29 | The Excel file should have columns for PrimarySMTP, GroupType, and DisplayName. These columns are used to get information about the groups from which the user should be removed. 30 | 31 | The function supports the following group types: 365Group, 365Distribution, 365MailEnabledSecurity, and 365Security. For each group type, it uses a different cmdlet to remove the user from the group. 32 | 33 | Connect-MgGraph -Scopes "Group.ReadWrite.All","Directory.AccessAsUser.All" - is needed to connect with correct scopes 34 | 35 | .LINK 36 | 37 | https://docs.microsoft.com/en-us/powershell/module/microsoft.graph.groups/?view=graph-powershell-1.0 38 | 39 | .LINK 40 | 41 | https://www.powershellgallery.com/packages/ImportExcel 42 | 43 | #> 44 | function Remove-CT365GroupByUserRole { 45 | [CmdletBinding(SupportsShouldProcess)] 46 | param( 47 | [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] 48 | [ValidateScript({ 49 | # First, check if the file has a valid Excel extension (.xlsx) 50 | if (-not(([System.IO.Path]::GetExtension($psitem)) -match "\.(xlsx)$")) { 51 | throw "The file path '$PSitem' does not have a valid Excel format. Please make sure to specify a valid file with a .xlsx extension and try again." 52 | } 53 | 54 | # Then, check if the file exists 55 | if (-not([System.IO.File]::Exists($psitem))) { 56 | throw "The file path '$PSitem' does not lead to an existing file. Please verify the 'FilePath' parameter and ensure that it points to a valid file (folders are not allowed)." 57 | } 58 | 59 | # Return true if both conditions are met 60 | $true 61 | })] 62 | [string]$FilePath, 63 | 64 | [Parameter(Mandatory)] 65 | [ValidateScript({ 66 | # Check if the email fits the pattern 67 | switch ($psitem) { 68 | { $psitem -notmatch "^\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$" } { 69 | throw "The provided email is not in the correct format." 70 | } 71 | Default { 72 | $true 73 | } 74 | } 75 | })] 76 | [string]$UserEmail, 77 | 78 | [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] 79 | [ValidateScript({ 80 | # Check if the domain fits the pattern 81 | switch ($psitem) { 82 | { $psitem -notmatch '^(((?!-))(xn--|_)?[a-z0-9-]{0,61}[a-z0-9]{1,1}\.)*(xn--)?[a-z]{2,}(?:\.[a-z]{2,})+$' } { 83 | throw "The provided domain is not in the correct format." 84 | } 85 | Default { 86 | $true 87 | } 88 | } 89 | })] 90 | [string]$Domain, 91 | 92 | [Parameter(Mandatory)] 93 | [string]$UserRole 94 | ) 95 | 96 | # Import Required Modules 97 | $ModulesToImport = "ImportExcel", "Microsoft.Graph.Groups", "Microsoft.Graph.Users", "PSFramework", "ExchangeOnlineManagement" 98 | Import-Module $ModulesToImport 99 | 100 | # Connect to Exchange Online 101 | Connect-ExchangeOnline -UserPrincipalName $UserPrincipalName -ShowProgress $true 102 | 103 | # Connect to Microsoft Graph 104 | $Scopes = @("Group.ReadWrite.All", "Directory.AccessAsUser.All") 105 | $Context = Get-MgContext 106 | 107 | if ([string]::IsNullOrEmpty($Context) -or ($Context.Scopes -notmatch [string]::Join('|', $Scopes))) { 108 | Connect-MGGraph -Scopes $Scopes 109 | } 110 | 111 | $excelData = Import-Excel -Path $FilePath -WorksheetName $UserRole 112 | 113 | if ($PSCmdlet.ShouldProcess("Remove user from groups from Excel file")) { 114 | foreach ($row in $excelData) { 115 | $GroupName = $row.PrimarySMTP += "@$domain" 116 | $GroupType = $row.Type 117 | $DisplayName = $row.DisplayName 118 | 119 | if ($PSCmdlet.ShouldProcess("Remove user $UserEmail from $GroupType group $GroupName")) { 120 | try { 121 | Write-PSFMessage -Level Output -Message "Removing $UserEmail from $($GroupType):'$GroupName'" -Target $UserEmail 122 | switch -Regex ($GroupType) { 123 | "^365Group$" { 124 | Remove-UnifiedGroupLinks -Identity $GroupName -LinkType "Members" -Links $UserEmail -Confirm:$false 125 | } 126 | "^(365Distribution|365MailEnabledSecurity)$" { 127 | Remove-DistributionGroupMember -Identity $GroupName -Member $UserEmail -Confirm:$false 128 | } 129 | "^365Security$" { 130 | $user = Get-MgUser -Filter "userPrincipalName eq '$UserEmail'" 131 | $ExistingGroup = Get-MgGroup -Filter "DisplayName eq '$($DisplayName)'" 132 | if ($ExistingGroup) { 133 | Remove-MgGroupMemberByRef -GroupId $ExistingGroup.Id -DirectoryObjectId $User.Id 134 | Write-PSFMessage -Level Output -Message "User $UserEmail successfully removed from $GroupType group $GroupName" -Target $UserEmail 135 | } 136 | else { 137 | Write-PSFMessage -Level Warning -Message "No group found with the name: $GroupName" -Target $GroupName 138 | } 139 | 140 | } 141 | default { 142 | Write-PSFMessage -Level Warning -Message "Unknown group type: $GroupType" -Target $GroupType 143 | } 144 | 145 | } 146 | Write-PSFMessage -Level Output -Message "Removed $UserEmail from $($GroupType):'$GroupName' sucessfully" -Target $UserEmail 147 | } 148 | catch { 149 | Write-PSFMessage -Level Error -Message "$($_.Exception.Message) - Error removing user $UserEmail from $($GroupType):'$GroupName'" -Target $UserEmail 150 | } 151 | } 152 | } 153 | } 154 | 155 | # Disconnect Exchange Online and Microsoft Graph sessions 156 | Disconnect-ExchangeOnline -Confirm:$false 157 | if (-not [string]::IsNullOrEmpty($(Get-MgContext))) { 158 | Disconnect-MgGraph 159 | } 160 | } -------------------------------------------------------------------------------- /Functions/Public/Remove-CT365SharePointSite.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Deletes SharePoint Online sites based on the data from an Excel file. 4 | 5 | .DESCRIPTION 6 | The `Remove-365CTSharePointSite` function connects to SharePoint Online(PnP) using the provided admin URL and imports site data from the specified Excel file. It then attempts to delete each site based on the data. 7 | 8 | .PARAMETER FilePath 9 | The path to the Excel file containing the SharePoint site data. The file must exist and have an .xlsx extension. 10 | 11 | .PARAMETER AdminUrl 12 | The SharePoint Online admin URL. 13 | 14 | .PARAMETER Domain 15 | The domain information required for the SharePoint site creation. 16 | 17 | .PARAMETER PermanentlyDelete 18 | This will completely delete the SharePoint site so you can reuse that site address again. 19 | 20 | .EXAMPLE 21 | Remove-CT365SharePointSite -FilePath "C:\path\to\file.xlsx" -AdminUrl "domainname.sharepoint.com" -Domain "contoso.com" 22 | 23 | This example removes SharePoint sites using the data from the "file.xlsx" and connects to SharePoint Online using the provided admin URL. 24 | 25 | .NOTES 26 | To use this, please make sure that you the sites have been created at least 6 minutes prior, or it won't work. Also it will say "Group not found" but still works as of 10/23/2023. Open issue in GitHub for more information. 27 | Make sure you have the necessary modules installed: ImportExcel, PnP.PowerShell, and PSFramework. 28 | 29 | .LINK 30 | https://docs.microsoft.com/powershell/module/sharepoint-pnp/new-pnpsite 31 | #> 32 | function Remove-CT365SharePointSite { 33 | [CmdletBinding()] 34 | param ( 35 | [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] 36 | [ValidateScript({ 37 | # First, check if the file has a valid Excel extension (.xlsx) 38 | if (-not(([System.IO.Path]::GetExtension($psitem)) -match "\.(xlsx)$")) { 39 | throw "The file path '$PSitem' does not have a valid Excel format. Please make sure to specify a valid file with a .xlsx extension and try again." 40 | } 41 | 42 | # Then, check if the file exists 43 | if (-not([System.IO.File]::Exists($psitem))) { 44 | throw "The file path '$PSitem' does not lead to an existing file. Please verify the 'FilePath' parameter and ensure that it points to a valid file (folders are not allowed)." 45 | } 46 | 47 | # Return true if both conditions are met 48 | $true 49 | })] 50 | [string]$FilePath, 51 | 52 | [Parameter(Mandatory = $false)] 53 | [ValidateScript({ 54 | if ($_ -match '^[a-zA-Z0-9]+\.sharepoint\.[a-zA-Z0-9]+$') { 55 | $true 56 | } 57 | else { 58 | throw "The URL $_ does not match the required format." 59 | } 60 | })] 61 | [string]$AdminUrl, 62 | 63 | 64 | [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] 65 | [ValidateScript({ 66 | switch ($psitem) { 67 | { $psitem -notmatch '^(((?!-))(xn--|_)?[a-z0-9-]{0,61}[a-z0-9]{1,1}\.)*(xn--)?[a-z]{2,}(?:\.[a-z]{2,})+$' } { 68 | throw "The provided domain is not in the correct format." 69 | } 70 | Default { 71 | $true 72 | } 73 | } 74 | })] 75 | [string]$Domain, 76 | 77 | [switch]$PermanentlyDelete 78 | ) 79 | 80 | begin { 81 | # Set default message parameters. 82 | $PSDefaultParameterValues = @{ 83 | "Write-PSFMessage:Level" = "OutPut" 84 | "Write-PSFMessage:Target" = "Preparation" 85 | } 86 | 87 | # Import required modules. 88 | $ModulesToImport = "ImportExcel", "PnP.PowerShell", "PSFramework" 89 | Import-Module $ModulesToImport 90 | 91 | try { 92 | # Import site data from Excel. 93 | $SiteData = Import-Excel -Path $FilePath -WorksheetName "Sites" 94 | } 95 | catch { 96 | # Log an error and exit if importing site data fails. 97 | Write-PSFMessage -Message "Failed to import SharePoint Site data from Excel file." -Level Error 98 | return 99 | } 100 | } 101 | 102 | process { 103 | foreach ($site in $siteData) { 104 | 105 | # Join Admin URL and Site Url 106 | $siteUrl = "https://$AdminUrl/sites/$($site.Url)" 107 | 108 | try { 109 | # Set the message target to the site's title. 110 | $PSDefaultParameterValues["Write-PSFMessage:Target"] = $site.Title 111 | 112 | # Log a message indicating site deletion. 113 | Write-PSFMessage -Message "Deleting SharePoint Site: '$($site.Title)'" 114 | 115 | # If PermanentlyDelete switch is set, prioritize those actions 116 | if ($PermanentlyDelete) { 117 | switch -Regex ($site.SiteType) { 118 | "^TeamSite$" { 119 | $removePnPM365GroupPermSplat = @{ 120 | Identity = $site.Title 121 | ErrorAction = 'Stop' 122 | } 123 | 124 | $removePnPTenantSiteSplat = @{ 125 | Url = $siteUrl 126 | ErrorAction = 'Stop' 127 | Force = $true 128 | FromRecycleBin = $true 129 | } 130 | 131 | Remove-PnPDeletedMicrosoft365Group @removePnPM365GroupPermSplat 132 | Write-PSFMessage -Message "Group'$($Site.Title)' Deleted from Recycle Bin Successfully!" 133 | Remove-PnPTenantSite @removePnPTenantSiteSplat 134 | Write-PSFMessage -Message "Permanently deleted SharePoint Site: '$($siteUrl)'" 135 | continue 136 | } 137 | "^(CommunicationSite|TeamSiteWithoutMicrosoft365Group)$" { 138 | $removePnPTenantSiteSplat = @{ 139 | Url = $siteUrl 140 | ErrorAction = 'Stop' 141 | Force = $true 142 | } 143 | Remove-PnPTenantDeletedSite @removePnPTenantSiteSplat 144 | Write-PSFMessage -Message "Permanently deleted SharePoint Site: '$($siteUrl)'" 145 | continue 146 | } 147 | default { 148 | Write-PSFMessage "Unknown site type: $($site.SiteType) for site $($site.Title). Skipping." -Level Error 149 | continue 150 | } 151 | } 152 | } 153 | 154 | else { 155 | # If not permanently deleting, proceed with regular deletion 156 | switch -Regex ($site.SiteType) { 157 | "^TeamSite$" { 158 | $removePnPM365GroupSplat = @{ 159 | Identity = $site.Title 160 | ErrorAction = 'Stop' 161 | } 162 | Remove-PnPMicrosoft365Group @removePnPM365GroupSplat 163 | Write-PSFMessage -Message "Successfully deleted Group Site: '$($site.Title)'" 164 | 165 | } 166 | "^(CommunicationSite|TeamSiteWithoutMicrosoft365Group)$" { 167 | $removePnPSiteSplat = @{ 168 | Url = $siteUrl 169 | ErrorAction = "Stop" 170 | Force = $true 171 | } 172 | remove-PnPTenantSite @removePnPSiteSplat 173 | Write-PSFMessage -Message "Successfully deleted SharePoint Site: '$($siteUrl)'" 174 | } 175 | default { 176 | Write-PSFMessage "Unknown site type: $($site.SiteType) for site $($site.Title). Skipping." -Level Error 177 | continue 178 | } 179 | } 180 | } 181 | } 182 | catch [System.Net.WebException], [Microsoft.SharePoint.Client.ClientRequestException] { 183 | Write-PSFMessage -Message "Network or SharePoint client error occurred for site: '$($site.Title)'" -Level Error 184 | Write-PSFMessage -Message "Error details: $($_.Exception.Message)" -Level Error 185 | } 186 | catch { 187 | Write-PSFMessage -Message "An unexpected error occurred for site: '$($site.Title)'" -Level Error 188 | Write-PSFMessage -Message "Error details: $($_.Exception.Message)" -Level Error 189 | Write-PSFMessage -Message "Stack Trace: $($_.Exception.StackTrace)" -Level Error 190 | } 191 | } 192 | } 193 | 194 | end { 195 | Write-PSFMessage "SharePoint site deletion process completed." 196 | Disconnect-PnPOnline 197 | } 198 | } -------------------------------------------------------------------------------- /Functions/Public/Remove-CT365Teams.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Removes Microsoft 365 Teams based on the provided Excel data. 4 | 5 | .DESCRIPTION 6 | The Remove-CT365Teams function connects to SharePoint Online, reads a list of Teams from an Excel file, and then removes each team. The function provides feedback on the process using the Write-PSFMessage cmdlet. 7 | 8 | .PARAMETER FilePath 9 | The path to the Excel file containing the list of Teams to remove. The file should have a worksheet named "Teams" and must be in .xlsx format. 10 | 11 | .PARAMETER AdminUrl 12 | The URL of the SharePoint Online admin center. This is used for connecting to SharePoint Online. 13 | 14 | .PARAMETER ChannelColumns 15 | Array of channel column names. The default values are "Channel1Name" and "Channel2Name". 16 | 17 | .EXAMPLE 18 | Remove-CT365Teams -FilePath "C:\Path\To\File.xlsx" -AdminUrl "yourtenant-admin.sharepoint.com" 19 | 20 | This example will connect to the SharePoint Online admin center using the provided AdminUrl, read the Teams from the specified Excel file, and proceed to remove each team. 21 | 22 | .NOTES 23 | - Ensure you have the necessary modules ("ImportExcel","PnP.PowerShell","PSFramework","Microsoft.Identity.Client") installed before running this function. 24 | - Always backup your Teams data before using this function to avoid unintended data loss. 25 | - This function has a built-in delay of 5 seconds between team removals to ensure proper deletion. 26 | 27 | #> 28 | function Remove-CT365Teams { 29 | [CmdletBinding()] 30 | param ( 31 | # Validate the Excel file path. 32 | [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] 33 | [ValidateScript({ 34 | # First, check if the file has a valid Excel extension (.xlsx) 35 | if (-not(([System.IO.Path]::GetExtension($psitem)) -match "\.(xlsx)$")) { 36 | throw "The file path '$PSitem' does not have a valid Excel format. Please make sure to specify a valid file with a .xlsx extension and try again." 37 | } 38 | 39 | # Then, check if the file exists 40 | if (-not([System.IO.File]::Exists($psitem))) { 41 | throw "The file path '$PSitem' does not lead to an existing file. Please verify the 'FilePath' parameter and ensure that it points to a valid file (folders are not allowed)." 42 | } 43 | 44 | # Return true if both conditions are met 45 | $true 46 | })] 47 | [string]$FilePath, 48 | 49 | [Parameter(Mandatory = $false)] 50 | [ValidateScript({ 51 | if ($_ -match '^[a-zA-Z0-9]+\.sharepoint\.[a-zA-Z0-9]+$') { 52 | $true 53 | } 54 | else { 55 | throw "The URL $_ does not match the required format." 56 | } 57 | })] 58 | [string]$AdminUrl, 59 | 60 | 61 | [Parameter(Mandatory = $false)] 62 | [string[]]$ChannelColumns = @("Channel1Name", "Channel2Name") 63 | ) 64 | 65 | begin { 66 | # Import required modules. 67 | $ModulesToImport = "ImportExcel", "PnP.PowerShell", "PSFramework", "Microsoft.Identity.Client" 68 | Import-Module $ModulesToImport 69 | 70 | try { 71 | # Import site data from Excel. 72 | $SiteData = Import-Excel -Path $FilePath -WorksheetName "Teams" 73 | } 74 | catch { 75 | # Log an error and exit if importing site data fails. 76 | Write-PSFMessage -Message "Failed to import SharePoint Site data from Excel file." -Level Error 77 | return 78 | } 79 | } 80 | 81 | process { 82 | foreach ($team in $SiteData) { 83 | # Get the GroupId for the Team based on its name 84 | $teamObj = Get-PnPTeamsTeam | Where-Object { $_.DisplayName -eq $team.TeamName } 85 | 86 | # Continue to the next iteration if no matching team is found 87 | if (-not $teamObj) { continue } 88 | 89 | $teamGroupId = $teamObj.GroupId 90 | 91 | # Display the team name that's being removed using Write-PSFMessage 92 | Write-PSFMessage -Message "Removing team: $($team.TeamName) with GroupId: $teamGroupId" -Level Host 93 | 94 | # Remove the Team using the GroupId 95 | Remove-PnPTeamsTeam -Identity $teamGroupId -Force 96 | 97 | # Introduce a delay of 5 seconds 98 | Start-Sleep -Seconds 5 99 | 100 | # Check if the team still exists 101 | $teamCheck = Get-PnPTeamsTeam | Where-Object { $_.GroupId -eq $teamGroupId } 102 | 103 | # Provide feedback based on team removal status 104 | $messageLevel = if ($teamCheck) { "Warning" } else { "Host" } 105 | $messageContent = if ($teamCheck) { "Failed to remove team: $($team.TeamName)" } else { "Successfully removed team: $($team.TeamName)" } 106 | 107 | Write-PSFMessage -Message $messageContent -Level $messageLevel 108 | } 109 | } 110 | 111 | 112 | end { 113 | # Disconnect from PnP 114 | Disconnect-PnPOnline 115 | Write-PSFMessage -Message "Teams removal completed." 116 | } 117 | } -------------------------------------------------------------------------------- /Functions/Public/Remove-CT365User.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Removes a user from Microsoft 365 based on the provided Excel data. 4 | 5 | .DESCRIPTION 6 | The Remove-CT365User function connects to the Microsoft Graph, reads user data from the provided Excel file, 7 | and attempts to remove each user listed in the file from Microsoft 365. 8 | 9 | .PARAMETER FilePath 10 | Specifies the full path to the Excel file that contains the user data. This parameter is mandatory. 11 | 12 | .PARAMETER Domain 13 | Specifies the domain that will be concatenated with the UserPrincipalName to form a valid email address. This parameter is mandatory. 14 | 15 | .EXAMPLE 16 | Remove-CT365User -FilePath "C:\Path\to\file.xlsx" -Domain "example.com" 17 | 18 | This command attempts to remove the users listed in the "file.xlsx" Excel file from the "example.com" domain. 19 | 20 | .INPUTS 21 | System.String. You can pipe a string that contains the file path and domain to Remove-CT365User. 22 | 23 | .OUTPUTS 24 | System.String. Outputs a message for each attempted user removal, indicating success or failure. 25 | 26 | .NOTES 27 | This function requires the Microsoft.Graph.Users, ImportExcel, and PSFramework modules. Make sure to install them using Install-Module before running this function. 28 | 29 | .LINK 30 | https://docs.microsoft.com/en-us/powershell/module/microsoft.graph.users/?view=graph-powershell-1.0 31 | 32 | .LINK 33 | https://www.powershellgallery.com/packages/ImportExcel 34 | 35 | .LINK 36 | https://psframework.org/documentation/commands/PSFramework.html 37 | #> 38 | function Remove-CT365User { 39 | [CmdletBinding()] 40 | param ( 41 | [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] 42 | [ValidateScript({ 43 | # First, check if the file has a valid Excel extension (.xlsx) 44 | if (-not(([System.IO.Path]::GetExtension($psitem)) -match "\.(xlsx)$")) { 45 | throw "The file path '$PSitem' does not have a valid Excel format. Please make sure to specify a valid file with a .xlsx extension and try again." 46 | } 47 | 48 | # Then, check if the file exists 49 | if (-not([System.IO.File]::Exists($psitem))) { 50 | throw "The file path '$PSitem' does not lead to an existing file. Please verify the 'FilePath' parameter and ensure that it points to a valid file (folders are not allowed)." 51 | } 52 | 53 | # Return true if both conditions are met 54 | $true 55 | })] 56 | [string]$FilePath, 57 | 58 | [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] 59 | [ValidateScript({ 60 | # Check if the domain fits the pattern 61 | switch ($psitem) { 62 | { $psitem -notmatch '^(((?!-))(xn--|_)?[a-z0-9-]{0,61}[a-z0-9]{1,1}\.)*(xn--)?[a-z]{2,}(?:\.[a-z]{2,})+$' } { 63 | throw "The provided domain is not in the correct format." 64 | } 65 | Default { 66 | $true 67 | } 68 | } 69 | })] 70 | [string]$Domain 71 | 72 | ) 73 | 74 | # Import Required Modules 75 | $ModulesToImport = "ImportExcel", "Microsoft.Graph.Users", "Microsoft.Graph.Groups", "Microsoft.Graph.Identity.DirectoryManagement", "Microsoft.Graph.Users.Actions", "PSFramework" 76 | Import-Module $ModulesToImport 77 | 78 | # Connect to Microsoft Graph 79 | $Scopes = @("User.ReadWrite.All") 80 | $Context = Get-MgContext 81 | 82 | if ([string]::IsNullOrEmpty($Context) -or ($Context.Scopes -notmatch [string]::Join('|', $Scopes))) { 83 | Connect-MGGraph -Scopes $Scopes 84 | } 85 | 86 | # Import user data from Excel file 87 | $userData = $null 88 | try { 89 | $userData = Import-Excel -Path $FilePath -WorksheetName Users 90 | } 91 | catch { 92 | Write-PSFMessage -Level Error -Message "Failed to import user data from Excel file." 93 | return 94 | } 95 | 96 | # Iterate through each user in the Excel file and delete them 97 | foreach ($user in $userData) { 98 | $NewUserParams = @{ 99 | UserPrincipalName = "$($user.UserName)@$domain" 100 | GivenName = $user.FirstName 101 | Surname = $user.LastName 102 | DisplayName = "$($user.Firstname) $($user.Lastname)" 103 | MailNickname = $user.UserName 104 | JobTitle = $user.Title 105 | Department = $user.Department 106 | } 107 | 108 | Write-PSFMessage -Level Output -Message "Removing user: '$($NewUserParams.UserPrincipalName)'" -Target $NewUserParams.UserName 109 | 110 | $userToRemove = Get-MgUser | Where-Object { $_.DisplayName -eq $NewUserParams.DisplayName } 111 | 112 | # Validate if the user exists 113 | if ($userToRemove) { 114 | Remove-MgUser -UserId $userToRemove.id 115 | 116 | # Check the user's existence 117 | $removedUser = Get-MgUser | Where-Object { $_.DisplayName -eq $NewUserParams.DisplayName } 118 | 119 | # Confirm that the user was removed 120 | if (-not $removedUser) { 121 | Write-PSFMessage -Level Output -Message "User $($NewUserParams.DisplayName) has been successfully removed." -Target $NewUserParams.DisplayName 122 | } 123 | else { 124 | Write-PSFMessage -Level Warning -Message "Failed to remove user $($NewUserParams.DisplayName)." -Target $NewUserParams.DisplayName 125 | } 126 | } 127 | else { 128 | Write-PSFMessage -Level Warning -Message "User $($NewUserParams.DisplayName) does not exist." -Target $NewUserParams.DisplayName 129 | } 130 | } 131 | 132 | # Disconnect Microsoft Graph Sessions 133 | if (-not [string]::IsNullOrEmpty($(Get-MgContext))) { 134 | Disconnect-MgGraph 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /Functions/Public/Set-CT365SPDistinctNumber.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Replaces specified numbers in an Excel worksheet, excluding certain columns. 4 | 5 | .DESCRIPTION 6 | The Set-CT365SPDistinctNumber function opens an Excel file and replaces occurrences of a specified number in a given worksheet. 7 | It excludes specific columns ("Template" and "TimeZone") from this operation. 8 | The primary reason for this is to be able to create new Sharepoint Team sites while the others are deleting. 9 | 10 | .PARAMETER FilePath 11 | The path to the Excel file that contains the worksheet to be modified. 12 | 13 | .PARAMETER WorksheetName 14 | The name of the worksheet within the Excel file where the replacements will be made. 15 | 16 | .PARAMETER FindNumber 17 | The number to find in the worksheet. This number will be replaced wherever it is found, except in the excluded columns. 18 | 19 | .PARAMETER ReplaceNumber 20 | The number that will replace the FindNumber in the worksheet. 21 | 22 | .EXAMPLE 23 | Set-CT365SPDistinctNumber -FilePath "C:\Documents\example.xlsx" -WorksheetName "Sheet1" -FindNumber "36" -ReplaceNumber "37" 24 | 25 | This command replaces all occurrences of the number 36 with 37 in the worksheet named "Sheet1" of the Excel file located at "C:\Documents\example.xlsx", excluding the "Template" and "TimeZone" columns. 26 | 27 | .INPUTS 28 | None. You cannot pipe objects to Set-CT365SPDistinctNumber. 29 | 30 | .OUTPUTS 31 | None. This function does not generate any output. 32 | 33 | .NOTES 34 | This function requires the ImportExcel module to be installed. 35 | 36 | .LINK 37 | https://github.com/dfinke/ImportExcel - The ImportExcel PowerShell module 38 | 39 | #> 40 | function Set-CT365SPDistinctNumber { 41 | [CmdletBinding()] 42 | param ( 43 | [Parameter(Mandatory)] 44 | [string]$FilePath, 45 | 46 | [Parameter(Mandatory)] 47 | [string]$WorksheetName, 48 | 49 | [Parameter(Mandatory)] 50 | [string]$FindNumber, 51 | 52 | [Parameter(Mandatory)] 53 | [string]$ReplaceNumber 54 | ) 55 | 56 | # Import the ImportExcel module 57 | Import-Module ImportExcel 58 | 59 | # Open the Excel package 60 | $excelPackage = Open-ExcelPackage -Path $FilePath 61 | 62 | try { 63 | $worksheet = $excelPackage.Workbook.Worksheets[$WorksheetName] 64 | if ($null -eq $worksheet) { 65 | throw "Worksheet '$WorksheetName' not found." 66 | } 67 | 68 | # Get the indices of the columns to exclude 69 | $excludedColumns = @("Template", "TimeZone").ForEach({ 70 | $worksheet.Dimension.Start.Column..$worksheet.Dimension.End.Column | 71 | Where-Object { $worksheet.Cells[1, $_].Text -eq $_ } | 72 | ForEach-Object { [OfficeOpenXml.ExcelCellAddress]::GetColumnLetter($_) } 73 | }) 74 | 75 | # Initialize a counter for replacements 76 | $replacementCount = 0 77 | 78 | # Find and replace the numbers, skipping the excluded columns 79 | $worksheet.Cells.Where({ 80 | $_.Value -like "*$FindNumber*" -and 81 | -not ($excludedColumns -contains [OfficeOpenXml.ExcelCellAddress]::GetColumnLetter($_.Start.Column)) 82 | }).ForEach({ 83 | if ($_ -ne $null -and $null -ne $_.Value -and $_.Value -like "*$FindNumber*") { 84 | $_.Value = $_.Value -replace $FindNumber, $ReplaceNumber 85 | $replacementCount++ 86 | } 87 | }) 88 | 89 | # Check if the number of replacements is as expected 90 | if ($replacementCount -eq 0) { 91 | throw "No replacements were made for the number '$FindNumber'." 92 | } elseif ($replacementCount -ne 12) { 93 | Write-PSFMessage -Message "Unexpected number of replacements: $replacementCount. Expected 12." -Level Error 94 | } else { 95 | Write-PSFMessage -Message "Exactly 12 replacements were made for the number '$FindNumber'." -Level Host 96 | } 97 | 98 | # Save and close the Excel package 99 | Close-ExcelPackage $excelPackage 100 | } 101 | catch { 102 | $excelPackage.Dispose() 103 | throw 104 | } 105 | } -------------------------------------------------------------------------------- /LabSources/365DataEnvironment.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevClate/365AutomatedLab/49fab66b5cea7cd3c76ce36c8023e330d031fb47/LabSources/365DataEnvironment.xlsx -------------------------------------------------------------------------------- /Presentations/02-08-2024-PowerShellPulse.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevClate/365AutomatedLab/49fab66b5cea7cd3c76ce36c8023e330d031fb47/Presentations/02-08-2024-PowerShellPulse.pdf -------------------------------------------------------------------------------- /Presentations/Summary.md: -------------------------------------------------------------------------------- 1 | Here will be a placeholder for all current and upcoming presentations. 2 | 3 | 02-08-2024 - PowerShell Pulse - Powershell Excel-lence 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Alt 365AutomatedLab Logo](https://github.com/DevClate/365AutomatedLab/blob/main/Static/365automatedlab.png?raw=true) 2 | 3 | # 365AutomatedLab 4 | ![PowerShell Gallery](https://img.shields.io/powershellgallery/dt/365AutomatedLab?label=Downloads&style=flat-square) 5 | 6 | This module will create a Microsoft 365 Test Environment using an excel workbook 7 | 8 | ## Table of Contents 9 | 10 | - [Project Summary](#project-summary) 11 | - [Requirements](#requirements) 12 | - [Installer](#installer) 13 | - [Current Functions](#current-functions) 14 | - [Data](#data) 15 | - [Getting Started](#getting-started) 16 | - [Changelog](https://github.com/DevClate/365AutomatedLab/blob/main/CHANGELOG.md) 17 | 18 | ## Project Summary 19 | 20 | I started this module to create a test environment for 365 as there wasn't one for groups, only users which Microsoft provides in there [365 Developer Program Environment](https://developer.microsoft.com/en-us/microsoft-365/dev-program). *As of April 23, 2024, the 365 Developer Program is still not allowing new accounts. I was hoping by now they would have changed their mind, but that isn't the case.* I was tired of having to come up with test user names, their information, and groups each time, or remembering which ones I had already created every time I wanted to do some testing. Let's be honest. we aren't in our test environment every day and all day. This started out as a quick function, then another function, then another....you can see how I quickly determined that this needed to be a module to keep everything organized and easily shareable to others. With that said, I'd love feedback(Create issues) and community help on this to really expand this. 21 | 22 | Due to the 365 Developer Program being on hold, I know it's not easy to get a test environment, but depending on the size of your company reach out to your consultant or who you purchase your licensing from. They may be able help you out. If you can't please test with a small data set just to ensure it works as you expected. 23 | 24 | ### Requirements 25 | 26 | - PowerShell Version: 27 | - 7.1+ Windows and Mac (Untested on Linux) 28 | - Modules 29 | - ImportExcel v7.8.2+ 30 | - ExchangeOnlineManagement v2.0.6+ 31 | - Microsoft.Graph.Users v1.17.0+ 32 | - Microsoft.Graph.Groups v1.17.0+ 33 | - Microsoft.Graph.Identity.DirectoryManagement v1.17.0+ 34 | - Microsoft.Graph.Users.Actions v1.17.0+ 35 | - PSFramework v1.8.289+ 36 | - PnP.PowerShell v2.2.0+ 37 | - Please read https://pnp.github.io/powershell/ if having issues with connecting 38 | - Microsoft.Identity.Client v4.50.0.0 39 | 40 | ### Installer 41 | 42 | 365AutomatedLab works on Windows and MacOS (M1+ and Intel) 43 | 44 | Run the below command to install 365AutomatedLab from the PowerShell Gallery. If you are running it on a server remove the -Scope parameter and run in an elevated session. 45 | 46 | ```PowerShell 47 | # Run first if you need to set Execution Policy 48 | Set-ExecutionPolicy RemoteSigned -Scope CurrentUser 49 | 50 | # Install 365AutomatedLab 51 | Install-Module -Name 365AutomatedLab -Scope CurrentUser 52 | ``` 53 | 54 | ### Current Functions 55 | 56 | - Create Users and assign license 57 | - New-CT365User 58 | - Remove Users 59 | - Remove-CT365User 60 | - Create the 4 types of Office 365 Groups/Distribution Lists 61 | - New-CT365Group 62 | - Remove the 4 types of Office 365 Groups/Distribution Lists 63 | - Remove-CT365Group 64 | - Assign User to any of their 4 Office 365 Groups/Distribution Lists by Job Title and Location 65 | - New-CT365GroupByUserRole 66 | - Remove User from any of their 4 Office 365 Groups/Distribution Lists by Job Title and Location 67 | - Remove-CT365GroupByUserRole 68 | - Copy worksheets name to a csv file so you can copy those location-titles into your ValidateSet 69 | - Copy-WorkSheetName 70 | - Create your own 365DataEnvironment workbook with the job roles for your organization 71 | - New-CT365DataEnvironment 72 | - Create a new SharePoint Site 73 | - New-CT365SharePointSite 74 | - Remove a SharePoint Site 75 | - Remove-CT365SharePointSite 76 | - Create Teams and channels 77 | - New-CT365Teams 78 | - Remove Teams and channels 79 | - Remove-CT365Teams 80 | - Export Users from production to import template 81 | - Export-CT365ProdUserToExcel 82 | - Replace distinct number for sharepoint site names 83 | - Set-CT365SPDistinctNumber 84 | - Delete all deleted Modern Microsoft 365 Groups 85 | - Remove-CT365AllDeletedM365Groups 86 | - Export Groups from production to import template 87 | - Export-CT365ProdGroupToExcel 88 | - Remove all SharePoint sites from the recycle bin 89 | - Remove-CT365AllSitesFromRecycleBin 90 | 91 | ### Data 92 | 93 | In LabSources you will find an excel file named 365DataEnvironment.xlsx that has 5 main tabs. Any additional tabs will be for different location-jobtitle tabs. You can use this workbook as is in your test environment, or use it as a template for your own data. 94 | 95 | - Users: This will have all of the user's information you are creating including licensing information 96 | - If you do not have a UsageLocation set, the licenses will not be added 97 | - Groups: This will have all the groups you want created 98 | - I do not have it assigning manager as of yet, but will in the future 99 | - Location-JobTitle: This will have all the groups that location and job title are suppose to have(Corresponds with JobRole Parameter). 100 | - Originally I had these in a validateset, but opted out. Let me know in the issues if they should be brought back 101 | - Teams: This will have all the Teams and Channels to be created 102 | - I only have it for 2 additional channels, but please let me know if you need more 103 | - Sites: This will have all of the SharePoint sites you want created 104 | - You can create the 4 different types of SharePoint sites as well has select the template you want 105 | 106 | In the future, I will have it so you can create random users using Doug Finke's PowerShellAI module and his ImportExcel module. Eventually, it will create the whole workbook! For now you can use ChatGPT with the prompt below to create your users. Feel free to customize the prompt for locations and departments that more match your environment if needed. 107 | 108 | ``` 109 | I need to create a Microsoft 365 test environment with 20 users. There must be a mixture of locations but they can only be in NY, FL, and CA. There must be a mixture of departments, but they can only be IT, HR, Accounting, and Marketing. The fields to create values for are FirstName, LastName, UserName, Title, Department, StreetAddress, City, State, PostalCode, Country, PhoneNumber, MobilePhone. The phone number and mobile number area codes should match the city and state they are in. This should be able to be pasted into an excel document. 110 | ``` 111 | 112 | ### Getting Started 113 | 114 | Once you have created your 365 Developer Program Environment, you can start adding users and groups. 115 | 116 | 1. Install the module by downloading the repository and copy into your PowerShell modules folder 117 | 2. Save the 365DataEnvironment.xlsx (located in LabSources) file on to your system 118 | 3. Run the below command to add users to your environment with their licensing 119 | 120 | 1. ```powershell 121 | New-CT365User -FilePath "C:\Path\to\365DataEnvironment.xlsx" -Domain "yourdomain.onmicrosoft.com" 122 | ``` 123 | 4. Run the below command to add groups to your environment 124 | 125 | 1. ```powershell 126 | New-CT365Group -FilePath "C:\Path\to\365DataEnvironment.xlsx" -UserPrincialName "user@yourdomain.onmicrosoft.com" -Domain "yourdomain.onmicrosoft.com" 127 | ``` 128 | 5. Run the below command to add a user to their groups per their location and title 129 | 130 | 1. ```powershell 131 | New-CT365GroupByUserRole -FilePath "C:\Path\to\365DataEnvironment.xlsx" -UserEmail "jdoe@yourdomain.onmicrosoft.com" -Domain "yourdomain.onmicrosoft.com" -UserRole "NY-IT" 132 | ``` 133 | 6. Run the below command to add Microsoft Teams and Channels to your environment 134 | 135 | 1. ```powershell 136 | New-CT365Teams -FilePath "C:\path\to\365DataEnvironment.xlsx" -AdminUrl "https://yourdomain.sharepoint.com" 137 | ``` 138 | 139 | Also definitely check out my [blog](https://www.clatent.com/) for more info on 365AutomatedLab and other projects I'm working on. 140 | -------------------------------------------------------------------------------- /Static/365automatedlab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevClate/365AutomatedLab/49fab66b5cea7cd3c76ce36c8023e330d031fb47/Static/365automatedlab.png -------------------------------------------------------------------------------- /Static/365automatedlabIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DevClate/365AutomatedLab/49fab66b5cea7cd3c76ce36c8023e330d031fb47/Static/365automatedlabIcon.png -------------------------------------------------------------------------------- /Tests/New-CT365Group.Tests.ps1: -------------------------------------------------------------------------------- 1 | BeforeAll { 2 | # Call Cmdlet 3 | $commandScriptPath = Join-Path -Path $PSScriptRoot -ChildPath '..\functions\public\New-CT365Group.ps1' 4 | 5 | . $commandScriptPath 6 | } 7 | 8 | Describe 'New-CT365Group Function' { 9 | Context 'When provided invalid parameters' { 10 | It 'Should throw an error for invalid domain format' { 11 | $filePath = $commandScriptPath 12 | $domain = "invalid_domain" 13 | 14 | { New-CT365Group -FilePath $filePath -Domain $domain } | Should -Throw 15 | } 16 | It 'Should throw an error for invalid file path' { 17 | $filePath = "C:\Invalid\Path\file.xlsx" 18 | $domain = "contoso.com" 19 | 20 | { New-CT365Group -FilePath $filePath -Domain $domain } | Should -Throw 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /Tests/New-CT365GroupByUserRole.Tests.ps1: -------------------------------------------------------------------------------- 1 | BeforeAll { 2 | # Call Cmdlet 3 | $commandScriptPath = Join-Path -Path $PSScriptRoot -ChildPath '..\functions\public\New-CT365GroupByUserRole.ps1' 4 | 5 | . $commandScriptPath 6 | } 7 | 8 | Describe 'New-CT365GroupByUserRole Function' { 9 | Context 'When provided invalid parameters' { 10 | It 'Should throw an error for invalid domain format' { 11 | $filePath = $commandScriptPath 12 | $domain = "invalid_domain" 13 | 14 | { New-CT365GroupByUserRole -FilePath $filePath -Domain $domain } | Should -Throw 15 | } 16 | It 'Should throw an error for invalid file path' { 17 | $filePath = "C:\Invalid\Path\file.xlsx" 18 | $domain = "contoso.com" 19 | 20 | { New-CT365GroupByUserRole -FilePath $filePath -Domain $domain } | Should -Throw 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /Tests/New-CT365SharePointSite.Tests.ps1: -------------------------------------------------------------------------------- 1 | BeforeAll { 2 | # Call Cmdlet 3 | $commandScriptPath = Join-Path -Path $PSScriptRoot -ChildPath '..\functions\public\New-CT365SharePointSite.ps1' 4 | 5 | . $commandScriptPath 6 | } 7 | 8 | Describe 'New-CT365SharePointSite Function' { 9 | Context 'When provided invalid parameters' { 10 | It 'Should throw an error for invalid domain format' { 11 | $filePath = $commandScriptPath 12 | $domain = "invalid_domain" 13 | 14 | { New-CT365SharePointSite -FilePath $filePath -Domain $domain } | Should -Throw 15 | } 16 | It 'Should throw an error for invalid file path' { 17 | $filePath = "C:\Invalid\Path\file.xlsx" 18 | $domain = "contoso.com" 19 | 20 | { New-CT365SharePointSite -FilePath $filePath -Domain $domain } | Should -Throw 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /Tests/New-CT365Teams.Tests.ps1: -------------------------------------------------------------------------------- 1 | BeforeAll { 2 | # Call Cmdlet 3 | $commandScriptPath = Join-Path -Path $PSScriptRoot -ChildPath '..\functions\public\New-CT365Teams.ps1' 4 | 5 | . $commandScriptPath 6 | } 7 | 8 | Describe 'New-CT365Teams Function' { 9 | Context 'When provided invalid parameters' { 10 | It 'Should throw an error for invalid domain format' { 11 | $filePath = $commandScriptPath 12 | $AdminUrl = "invalid_domain" 13 | 14 | { New-CT365Teams -FilePath $filePath -AdminUrl $AdminUrl } | Should -Throw 15 | } 16 | It 'Should throw an error for invalid file path' { 17 | $filePath = "C:\Invalid\Path\file.xlsx" 18 | $AdminUrl = "contoso.com" 19 | 20 | { New-CT365Teams -FilePath $filePath -AdminUrl $AdminUrl } | Should -Throw 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /Tests/New-CT365User.Tests.ps1: -------------------------------------------------------------------------------- 1 | BeforeAll { 2 | # Call Cmdlet 3 | $commandScriptPath = Join-Path -Path $PSScriptRoot -ChildPath '..\functions\public\New-CT365User.ps1' 4 | 5 | . $commandScriptPath 6 | } 7 | 8 | Describe 'New-CT365User Function' { 9 | Context 'When provided invalid parameters' { 10 | It 'Should throw an error for invalid domain format' { 11 | $filePath = $commandScriptPath 12 | $domain = "invalid_domain" 13 | 14 | { New-CT365User -FilePath $filePath -Domain $domain } | Should -Throw 15 | } 16 | It 'Should throw an error for invalid file path' { 17 | $filePath = "C:\Invalid\Path\file.xlsx" 18 | $domain = "contoso.com" 19 | 20 | { New-CT365User -FilePath $filePath -Domain $domain } | Should -Throw 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /Tests/Remove-CT365AllDeletedM365Groups.Tests.ps1: -------------------------------------------------------------------------------- 1 | BeforeAll { 2 | # Call Cmdlet 3 | $commandScriptPath = Join-Path -Path $PSScriptRoot -ChildPath '..\functions\public\Remove-CT365AllDeletedM365Groups.ps1' 4 | 5 | . $commandScriptPath 6 | } 7 | 8 | Describe 'Remove-CT365AllDeletedM365Groups Function' { 9 | Context 'When provided invalid parameters' { 10 | It 'Should throw an error for invalid url format' { 11 | $AdminUrl = "invalid_url" 12 | 13 | { Remove-CT365AllDeletedM365Groups -AdminUrl $AdminUrl } | Should -Throw 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /Tests/Remove-CT365Group.Tests.ps1: -------------------------------------------------------------------------------- 1 | BeforeAll { 2 | # Call Cmdlet 3 | $commandScriptPath = Join-Path -Path $PSScriptRoot -ChildPath '..\functions\public\Remove-CT365Group.ps1' 4 | 5 | . $commandScriptPath 6 | } 7 | 8 | Describe 'Remove-CT365Group Function' { 9 | Context 'When provided invalid parameters' { 10 | It 'Should throw an error for invalid domain format' { 11 | $filePath = $commandScriptPath 12 | $domain = "invalid_domain" 13 | 14 | { Remove-CT365Group -FilePath $filePath -Domain $domain } | Should -Throw 15 | } 16 | It 'Should throw an error for invalid file path' { 17 | $filePath = "C:\Invalid\Path\file.xlsx" 18 | $domain = "contoso.com" 19 | 20 | { Remove-CT365Group -FilePath $filePath -Domain $domain } | Should -Throw 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /Tests/Remove-CT365GroupByUserRole.Tests.ps1: -------------------------------------------------------------------------------- 1 | BeforeAll { 2 | # Call Cmdlet 3 | $commandScriptPath = Join-Path -Path $PSScriptRoot -ChildPath '..\functions\public\Remove-CT365GroupByUserRole.ps1' 4 | 5 | . $commandScriptPath 6 | } 7 | 8 | Describe 'Remove-CT365GroupByUserRole Function' { 9 | Context 'When provided invalid parameters' { 10 | It 'Should throw an error for invalid domain format' { 11 | $filePath = $commandScriptPath 12 | $domain = "invalid_domain" 13 | 14 | { Remove-CT365GroupByUserRole -FilePath $filePath -Domain $domain } | Should -Throw 15 | } 16 | It 'Should throw an error for invalid file path' { 17 | $filePath = "C:\Invalid\Path\file.xlsx" 18 | $domain = "contoso.com" 19 | 20 | { Remove-CT365GroupByUserRole -FilePath $filePath -Domain $domain } | Should -Throw 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /Tests/Remove-CT365SharePointSite.Tests.ps1: -------------------------------------------------------------------------------- 1 | BeforeAll { 2 | # Call Cmdlet 3 | $commandScriptPath = Join-Path -Path $PSScriptRoot -ChildPath '..\functions\public\Remove-CT365SharePointSite.ps1' 4 | 5 | . $commandScriptPath 6 | } 7 | 8 | Describe 'Remove-CT365SharePointSite Function' { 9 | Context 'When provided invalid parameters' { 10 | It 'Should throw an error for invalid domain format' { 11 | $filePath = $commandScriptPath 12 | $domain = "invalid_domain" 13 | 14 | { Remove-CT365SharePointSite -FilePath $filePath -Domain $domain } | Should -Throw 15 | } 16 | It 'Should throw an error for invalid file path' { 17 | $filePath = "C:\Invalid\Path\file.xlsx" 18 | $domain = "contoso.com" 19 | 20 | { Remove-CT365SharePointSite -FilePath $filePath -Domain $domain } | Should -Throw 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /Tests/Remove-CT365Teams.Tests.ps1: -------------------------------------------------------------------------------- 1 | BeforeAll { 2 | # Call Cmdlet 3 | $commandScriptPath = Join-Path -Path $PSScriptRoot -ChildPath '..\functions\public\Remove-CT365Teams.ps1' 4 | 5 | . $commandScriptPath 6 | } 7 | 8 | Describe 'Remove-CT365Teams Function' { 9 | Context 'When provided invalid parameters' { 10 | It 'Should throw an error for invalid domain format' { 11 | $filePath = $commandScriptPath 12 | $AdminUrl = "invalid_domain" 13 | 14 | { Remove-CT365Teams -FilePath $filePath -AdminUrl $AdminUrl } | Should -Throw 15 | } 16 | It 'Should throw an error for invalid file path' { 17 | $filePath = "C:\Invalid\Path\file.xlsx" 18 | $AdminUrl = "contoso.com" 19 | 20 | { Remove-CT365Teams -FilePath $filePath -AdminUrl $AdminUrl } | Should -Throw 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /Tests/Remove-CT365User.Tests.ps1: -------------------------------------------------------------------------------- 1 | BeforeAll { 2 | # Call Cmdlet 3 | $commandScriptPath = Join-Path -Path $PSScriptRoot -ChildPath '..\functions\public\Remove-CT365User.ps1' 4 | 5 | . $commandScriptPath 6 | } 7 | 8 | Describe 'Remove-CT365User Function' { 9 | Context 'When provided invalid parameters' { 10 | It 'Should throw an error for invalid domain format' { 11 | $filePath = $commandScriptPath 12 | $domain = "invalid_domain" 13 | 14 | { Remove-CT365User -FilePath $filePath -Domain $domain } | Should -Throw 15 | } 16 | It 'Should throw an error for invalid file path' { 17 | $filePath = "C:\Invalid\Path\file.xlsx" 18 | $domain = "contoso.com" 19 | 20 | { Remove-CT365User -FilePath $filePath -Domain $domain } | Should -Throw 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /Tests/Set-CT365SPDistinctNumber.Tests.ps1: -------------------------------------------------------------------------------- 1 | BeforeAll { 2 | # Call Cmdlet 3 | $commandScriptPath = Join-Path -Path $PSScriptRoot -ChildPath '..\functions\public\Set-CT365SPDistinctNumber.ps1' 4 | 5 | . $commandScriptPath 6 | } 7 | 8 | Describe 'Set-CT365SPDistinctNumber Function' { 9 | Context 'When provided invalid parameters' { 10 | It 'Should throw an error for invalid file path' { 11 | $filePath = "C:\Invalid\Path\file.xlsx" 12 | 13 | { Set-CT365SPDistinctNumber -FilePath $filePath -WorksheetName $WorksheetName -FindNumber 37 -ReplaceNumber 38 } | Should -Throw 14 | } 15 | } 16 | } --------------------------------------------------------------------------------