Follow

Create Workspace tenant

Table of Contents

 

Overview

You can have multiple tenants within your instance. These tenants are easily created with the PowerShell script posted below.

We recommend using this method when creating new tenants.

You can open PowerShell with administrator privileges, adjust two values and run the script. After filling in the Global Admin account credentials and giving the new environment a name, you'll be presented with an URL and you can start configuring your new Workspace tenant right away. 

 

Create tenant script

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 <[email protected]> , 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 = '[email protected]'
  }

  $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

Back to top