Create workspace tenants
In workspace365 you can have multiple tenants within your instance!
These tenants are easily created with the PowerShell script posted below.
As an admin it is possible to create a workspace from your instance. You need to update the $hostname and $provisioningKey to your own parameters. You can find the $provisioningKey in:
\inetpub\wwwroot\Workspace 365\Workspace 365\Configs\NDAW.Html.Front.config
As a partner you should be aware of your own hostname. Fill in the details in the script below and execute the .ps1 script.
#Requires -Version 5.1 using namespace System.Collections.Generic <# ########################################################################### ########################################################################### # Copyright (C) New Day at Work B.V. - All Rights Reserved # Unauthorized copying of this file, via any medium is strictly prohibited # Proprietary and confidential # Written by Workspace 365 <support@workspace365.net> , August 2020 ########################################################################### ########################################################################### # Prerequisites: # - Az Remote Management Components # - Workspace365 instance hostname # - Workspace365 instance provisioningskey ########################################################################### ########################################################################### #> # Workspace 365 URL/Hostname e.g. demo.workspace365.net $Workspace365Hostname = '' # Workspace 365 Instance provisioningskey $provisioningKey = '' if (-Not $psise) { [console]::Title = 'Workspace365 Environment Creation' [console]::WindowHeight = 50 [console]::WindowWidth = 160 } # Enforcing TLS 1.2 [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 Write-Verbose -Verbose 'One moment while we check available modules' $AvailableModules = (Get-module -ListAvailable).name Foreach ($Module in @('AzureAd','Az.accounts','Az.Resources')) { if ($Module -notin $AvailableModules) { Write-Host "Module : $Module is required but not currently installed" if ('^y' -match (Read-Host 'install now ?(y)') ) { Install-Module $Module -verbose } else { Write-Host 'Cannot continue, exiting' Start-Sleep 3 Return } } } Try { Import-Module AzureAD -ErrorAction Stop #Import-Module AzureAD -UseWindowsPowerShell -ErrorAction Stop Import-Module Az.Accounts -ErrorAction Stop Import-Module Az.Resources -ErrorAction Stop } Catch { $Psitem.exception Write-Warning 'There was a problem loading a required module, restart script and run as admin to try again.' Start-Sleep 3 Exit } <# These are the default values so you do not need to provide them everytime you run this script. #> Function GenerateAppKey ($fromDate, $durationInYears, $pw) { $endDate = $fromDate.AddYears($durationInYears) $keyId = (New-Guid).ToString(); $key = New-Object Microsoft.Open.AzureAD.Model.PasswordCredential($null, $endDate, $keyId, $fromDate, $pw) return $key } Function CreateAppKey($fromDate, $durationInYears, $pw) { $testKey = GenerateAppKey -fromDate $fromDate -durationInYears $durationInYears -pw $pw while ($testKey.Value -match "\+" -or $testKey.Value -match "/") { Write-Verbose "Secret contains + or / and may not authenticate correctly. Regenerating..." $pw = ComputePassword $testKey = GenerateAppKey -fromDate $fromDate -durationInYears $durationInYears -pw $pw } Write-Verbose "Secret doesn't contain + or /. Continuing..." $key = $testKey return $key } Function ComputePassword { $aesManaged = New-Object "System.Security.Cryptography.AesManaged" $aesManaged.Mode = [System.Security.Cryptography.CipherMode]::CBC $aesManaged.Padding = [System.Security.Cryptography.PaddingMode]::Zeros $aesManaged.BlockSize = 128 $aesManaged.KeySize = 256 $aesManaged.GenerateKey() return [System.Convert]::ToBase64String($aesManaged.Key) } Function Invoke-WorkspaceRestHelper { param ( [Parameter(ValueFromPipelineByPropertyName)] [ValidateSet('Get', 'Put', 'Post', 'Delete')] $Method, [Parameter(Mandatory,ValueFromPipelineByPropertyName)] [String]$Token, [Parameter(ValueFromPipelineByPropertyName)] $Body, [Alias('URI')] [Parameter(Mandatory,ValueFromPipelineByPropertyName)] [string]$URL ) ## Force .net TLS1.2 [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 Try { if ($Body -is [hashtable]) { $Body = $(ConvertTo-Json $Body -depth 10) } } Catch { Throw $psitem.Exception } $Header = @{ ProvisioningKey = $token } $RESTParam = @{ Uri = $URL Headers = $Header UseBasicParsing = $true Method = $Method Body = $Body Verbose = $False ContentType = 'application/json' } Try { Invoke-RestMethod @RESTParam -ErrorAction Stop } Catch { Write-Warning 'An error occured when attempting the current request, check the error below' $psitem.exception Pause } } Function Update-DefaultWorkspaceUrls { param ( $ProvisionInformation ) $ExchangeBody = @{ Servertype = 'Office365' DefaultEwsUrl = "https://outlook.office365.com/EWS/Exchange.asmx" } $SharePointBody = @{ AuthenticationType = 'Online' DefaultUrl = "https://$($ProvisionInformation.office365TenantName).sharepoint.com" } $ExchangeRequest = @{ Method = 'Put' Body = $ExchangeBody Url = "https://$workspace365Hostname/Provisioning/$($ProvisionInformation.workspace365environmentname)/ExchangeSettings" Token = $provisioningKey } $SharePointRequest = @{ Method = 'Put' Body = $SharePointBody Url = "https://$workspace365Hostname/Provisioning/$($ProvisionInformation.workspace365environmentname)/SharePointSettings" Token = $provisioningKey } if ('null' -eq (Invoke-WorkspaceRestHelper @ExchangeRequest) ) {Write-host 'Exchange autoprovisioned' -Fore Green} if ('null' -eq (Invoke-WorkspaceRestHelper @SharePointRequest) ) {Write-host 'SharePoint Autoprovisioned' -Fore Green} } Function RunProvisioning() { $o365user = $TenantInfo.Account.Id $TenantName = [regex]::Match( (Get-AzureADDomain).Name, '([^\.])*' ).groups[0].value $ADUser = Get-AzureAdUser -ObjectId $o365user $Firstname = $Aduser.GivenName $LastName = $ADuser.Surname $SecondaryEmailAddress = $ADuser.OtherMails if ([string]::IsNullOrWhiteSpace($LastName)) { $LastName = 'Workspace 365' } if ([string]::IsNullOrWhiteSpace($SecondaryEmailAddress) -or ($SecondaryEmailAddress -eq $o365user)) { $SecondaryEmailAddress = 'support@newdayatwork.com' } $ProvisionInformation = @{ office365User = $o365user office365TenantName = $TenantName office365Firstname = $Firstname office365Lastname = $LastName office365SecondaryEmail = $SecondaryEmailAddress workspace365environmentname = $EnvironmentName } CreateWorkspaceEnvironment($ProvisionInformation) } Function CreateWorkspaceEnvironment($ProvisionInformation) { $Workspace365environmentName = $ProvisionInformation.workspace365environmentname Write-Host 'Creating Workspace...' -ForegroundColor Yellow $requestBody = @{ EnvironmentName = $EnvironmentName EmailAddress = $ProvisionInformation.office365User FirstName = $ProvisionInformation.office365Firstname LastName = $ProvisionInformation.office365Lastname SecondaryEmailAddress = $ProvisionInformation.office365SecondaryEmail } $json = $requestBody | ConvertTo-Json Try { Invoke-RestMethod -Method Post -Uri "https://$workspace365Hostname/Provisioning/Environment/?version=2.0" -ContentType "application/json" -Header @{ 'ProvisioningKey' = $ProvisioningKey } -Body $json | Out-Null Write-Host 'Workspace provisioned successful' -ForegroundColor Green $isCreated = 'True' } Catch { Write-Host "Failed to create the Workspace, please check below error message:" -ForegroundColor Red $_.ErrorDetails.Message Break; } Update-DefaultWorkspaceUrls -ProvisionInformation $ProvisionInformation if($isCreated) { SetSSOMethodAndInfo WriteInformationToHost($Workspace365environmentName) } } Function SetSSOMethodAndInfo() { # Fill the parameters for oAuth2 $requestBody = @{ Authority = "https://login.windows.net/$($TenantInfo.TenantDomain)" clientId = $AzureSSOApplication.AppId Key = $appkey.value } # Set the paramaters for oAuth2 $json = $requestBody | ConvertTo-Json Invoke-RestMethod -Method Put -Uri "https://$workspace365Hostname/Provisioning/$environmentName/SingleSignOnSettings/OAuth2" -ContentType "application/json" -Header @{ 'ProvisioningKey' = $ProvisioningKey } -Body $json | Out-Null Write-host "Workspace365 succesfully provisioned, you can now log in to https://$workspace365Hostname/$EnvironmentName" -ForegroundColor "Green" } Function WriteInformationToHost(){ $url = "https://$workspace365Hostname/$Workspace365environmentName" Write-Host "----------------------------------------###----------------------------------------" -ForegroundColor Green Write-Host "Workspace: $Workspace365environmentName created successfully, you can now Sign In to the URL:" -ForegroundColor Green Write-Host "$url" -ForegroundColor Green Write-Host "----------------------------------------###----------------------------------------" -ForegroundColor Green # If you directly want to start the Workspace 365 URL, uncomment this Start-Process # Start-Process $url } Function New-ServicePrincipal { Param ( [String]$PrincipalName ) $SP = Get-AzureADServicePrincipal -SearchString $PrincipalName | Select-Object -First 1 if ($SP -is [Microsoft.Open.AzureAD.Model.DirectoryObject]) { $SP } } While ([string]::IsNullOrEmpty($Workspace365Hostname) -or ($Workspace365Hostname -match 'http' ) ) { $Workspace365Hostname = Read-Host 'There is no Workspace hostname in the script please provide the Url for the new deployment (eg ''yourCompany.workspace365.net'')' } While ([string]::IsNullOrEmpty($ProvisioningKey) ) { $ProvisioningKey = Read-Host 'You did not provide a provisioning key in the script, please provide one now' } Do { $EnvironmentName = (Read-Host -Prompt "Enter the new environment Name").tolower() } until(![string]::IsNullOrEmpty($EnvironmentName)) # Define the Workspace 365 environment name Write-Host 'We need you to authenticate to both AzureAD and AzureAz so you will get multiple login requests.' -Fore Green # Set the Azure AD Application name $ApplicationName = "Workspace 365 - $EnvironmentName" # Modify the homePage, appIdURI and logoutURI values to whatever valid URI you like. They don't need to be actual addresses. $homePage = "https://$workspace365Hostname/$EnvironmentName/SignIn" $appIdURI = "https://$workspace365Hostname/$EnvironmentName/OAuth2/HandleAuthorityResponse" $logoutURI = "https://$workspace365Hostname/$EnvironmentName/SignOut" #Login-AzureRmAccount Do { try { Connect-AzAccount -ErrorAction stop| out-Null $AzLoggedon = $true } catch { Write-Warning 'Logon cancelled or failed' start-sleep 3 exit } } until ($true -eq $AzLoggedon) [array]$Tenant = Get-AzTenant if ($tenant.count -gt 1) { $Tenant = $Tenant | Out-GridView -Title 'Select the correct Tenant' -OutputMode Single } Do { Try { $TenantInfo = Connect-AzureAd -TenantId $Tenant.id -Verbose -ErrorAction Stop $ADLoggedOn = $true } Catch { Write-Warning 'Logon cancelled or failed' Start-Sleep 3 } } until ($true -eq $ADLoggedOn) # We check if there is already an application for this url $ExistingApp = Get-AzureADApplication -SearchString $ApplicationName if ($ExistingApp) { Write-Warning "An App with the Name $ApplicationName already exists. Please review and delete the Azure AD App registration and try again." Pause } Write-verbose 'Collecting the required ServicePrincipals' $PowerBiSP = New-ServicePrincipal -PrincipalName 'Power BI Service' $AnalysisSP = New-ServicePrincipal -PrincipalName 'Azure Analysis Services' $GraphSP = New-ServicePrincipal -PrincipalName 'Microsoft Graph' if ($null -eq $GraphSP) { Try { Write-Verbose "Attempting to create Service Princpal" New-AzureADServicePrincipal -AppId '00000003-0000-0000-c000-000000000000' } Catch { } $GraphSP = New-ServicePrincipal -PrincipalName 'Microsoft Graph' } ## Windows Virtual Desktop # There are multiple with identical names... this function gets the 1st one, if you need the second, # Change the function and use -skip 1 in 99% of all situations the current code works fine. # '9cdead84-a844-4324-93f2-b2e6bb768d07' # '5a0aa725-4958-4b0c-80a9-34562e23f3b7' $WVDSP = New-ServicePrincipal -PrincipalName 'Windows Virtual Desktop' Write-verbose 'Creating ResourceAccess objects' $GraphSPRequiredAccess = [Microsoft.Open.AzureAD.Model.RequiredResourceAccess]::new() $PowerBiSPRequiredAccess = [Microsoft.Open.AzureAD.Model.RequiredResourceAccess]::new() $WVDSPRequiredAccess = [Microsoft.Open.AzureAD.Model.RequiredResourceAccess]::new() Write-verbose 'Populating Resource Access Objects' $GraphSPRequiredAccess.ResourceAppId = $GraphSP.AppId $PowerBiSPRequiredAccess.ResourceAppId = $PowerBiSP.AppId $WVDSPRequiredAccess.ResourceAppId = $WVDSP.AppId Write-Verbose 'Populating delegated permissions' # We are not adding the read's when we have readwrite as well, not that it matters much. [array]$GraphSPPermissionsSubset = 'Directory.ReadWrite.All','Calendars.ReadWrite.Shared','Contacts.ReadWrite.Shared','EWS.AccessAsUser.All','Files.ReadWrite.All','Mail.ReadWrite.Shared','Mail.Send','Mail.Send.Shared','People.Read', 'Sites.FullControl.All','Tasks.ReadWrite.Shared','User.Read','openid' [array]$PowerBIPermissionSubset = 'Dashboard.Read.All', 'Dataset.Read.All', 'Dataset.ReadWrite.All', 'Group.Read.All', 'Metadata.View_Any','Report.Read.All' [array]$WVDPermissionSubset = 'user_impersonation','User.Access' ## Graph $AllPermissions = $GraphSP.Oauth2Permissions $DelegatedPermissions = @() Foreach ($permission in $GraphSPPermissionsSubset) { Write-verbose $permission $PermissionId = ($allpermissions|Where-Object {$_.value -eq $permission}).Id if ($null -ne $PermissionId) { $DelegatedPermissions += New-Object -TypeName "Microsoft.Open.AzureAD.Model.ResourceAccess" -ArgumentList $PermissionId,"Scope" Write-verbose "added $PermissionId" } } $GraphSPRequiredAccess.ResourceAccess = $DelegatedPermissions ## PowerBI $AllPermissions = $PowerBiSP.Oauth2Permissions $DelegatedPermissions = @() Foreach ($permission in $PowerBIPermissionSubset) { Write-verbose $permission $PermissionId = ($Allpermissions|Where-Object {$_.value -eq $permission}).Id if ($null -ne $PermissionId) { $DelegatedPermissions += New-Object -TypeName "Microsoft.Open.AzureAD.Model.ResourceAccess" -ArgumentList $PermissionId,"Scope" Write-verbose "added $PermissionId" } } $PowerBiSPRequiredAccess.ResourceAccess = $DelegatedPermissions ## WVD $Allpermissions = $WVDSP.Oauth2Permissions $DelegatedPermissions = @() Foreach ($permission in $WVDPermissionSubset) { Write-verbose $permission $PermissionId = ($allpermissions|Where-Object {$_.value -eq $permission}).Id if ($null -ne $PermissionId) { $DelegatedPermissions += New-Object -TypeName "Microsoft.Open.AzureAD.Model.ResourceAccess" -ArgumentList $PermissionId,"Scope" Write-verbose "added $PermissionId" } } $WVDSPRequiredAccess.ResourceAccess = $DelegatedPermissions[0] Write-Verbose 'Putting all permissions objects together' $AppDelegatedPermissions = [List[Microsoft.Open.AzureAD.Model.RequiredResourceAccess]]::new() if ($null -ne $GraphSP) { $AppDelegatedPermissions.Add($GraphSPRequiredAccess) } if ($null -ne $PowerBiSP) { $AppDelegatedPermissions.Add($PowerBiSPRequiredAccess) } if ($null -ne $WVDSP) { $AppDelegatedPermissions.Add($WVDSPRequiredAccess) } Write-Verbose 'Generate credentials for the application' $pw = ComputePassword $appKey = CreateAppKey -fromDate (Get-Date) -durationInYears 99 -pw $pw ## Creating the Actual Application $NewAzureApp = @{ DisplayName = $ApplicationName HomePage = $Homepage ReplyUrls = $appIdURI IdentifierUris = $HomePage LogoutUrl = $LogoutURI PasswordCredentials = $AppKey RequiredResourceAccess = $AppDelegatedPermissions } Write-Host "Creating Azure AD App for $($Tenant.Name)" $AzureSSOApplication = New-AzureADApplication @NewAzureApp Write-Host "Azure AD App created : $($AzureSSOApplication.DisplayName)" # Provisioning Workspace RunProvisioning