Tag Archives: PowerShell

Useful PowerShell Scripts for Managing Classes in Microsoft Teams

So, you have school data sync setup and all of your class teams have been generated in Microsoft Teams. Teachers are eager to start using it for extending the classroom or remote learning. Teachers then realise that students can do things that they were not aware of and request for some rights to be restricted.

Here is a list of useful PowerShell scripts to help you manage some of the most common issues that schools face.

  • – Allow teachers to delete student messages
  • – Stop students emailing the class group
  • – Disable chat for students
  • – Calling and Live Event Policies

Allow teachers to delete student messages

It’s surprising that this is not enabled as standard. Owners in class teams cannot delete member messages unless a custom message policy is set.

Create a messaging policy in the Teams Admin centre

Create a new messaging policy and select “Owners can delete sent messages”

Create custom message policy in Teams
Owners can delete messages
Apply Custom Message Policy using PowerShell
 

This needs to be run as a global admin. The variables at the top of the script should be changed to the AAD (or synced AD) group that you want to apply the messaging policy to and the message policy name.

#Variables to change - add the AAD group and custom message policy name here
$ADSecurityGroupNameToApplyPolicyTo="All Teachers"
$customMessagePolicyName="CustomTeacherMessagingPolicy1"

# Install AzureAD PowerShell if you don't already have it - commented out below
# install-module azuread

#Import modules if you haven't already
Import-Module SkypeOnlineConnector
Import-Module AzureAD

#Connect to Skype and Azure AD
$userCredential = Get-Credential
$sfbSession = New-CsOnlineSession -Credential $userCredential
Import-PSSession $sfbSession
Connect-AzureAD -Credential $userCredential

$GroupUsers = Get-AzureADGroup -ALL $true -Filter "DisplayName eq '$ADSecurityGroupNameToApplyPolicyTo'" | Get-AzureADGroupMember -ALL $true | select mail
 
foreach ($GroupUser in $GroupUsers)
{
	$userEmail=$GroupUser.Mail
	write-host "Processing $userEmail"
	Grant-CsTeamsMessagingPolicy -PolicyName "$customMessagePolicyName" -Identity "$userEmail"
}

Stop students emailing the class group

Once a student receives a welcome message into a group, they may reply back to it or find it in the address list and start a large group email.

In the script below connect to Microsoft Exchange PowerShell. You should update the variables with an AD security group for students to apply the policy to. To ensure you only apply this to the relevant teams, use the wildcard search to filter them. In this example we are assuming teams have been named in a format of SchoolCode-AcademicYear-ClassName so we can set the wildcard to only apply this setting to Teams starting with SCH-2019.

######Replace the following variables if necessary##########
$studentADSecurityGroup ="All Students"   #AD Group for all students
$wildcardsearch="SCH-2019*"                #Wildcard for Teams display name - Search for Teams beginning with ....  
###########################################################

$MyCredential = Get-Credential
$Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://outlook.office365.com/powershell-liveid/ -Credential $MyCredential -Authentication Basic -AllowRedirection
Import-PSSession $Session -AllowClobber
$groups = Get-UnifiedGroup -ResultSize 20000 -SortBy DisplayName -Identity "$wildcardsearch" | Select DisplayName,WhenCreated,Id
 
foreach ($group in $groups)
{
    $teamName = $group.DisplayName
    Write-Host "restricting group emails on $teamName for $studentADSecurityGroup"
    Set-UnifiedGroup -Identity "$teamName" -RejectMessagesFromSendersOrMembers "$studentADSecurityGroup"
}

Disable chat for students

Teams is a safe environment for students to chat, chats can be audited and monitored more closely than if they where to use WhatsApp or snapchat outside of the school systems. However, there are some situations where it might require turning off for safeguarding reasons.

Create message policy in Teams admin centre
Teams message policy

Click “Add” to create a new message policy and turn off the chat setting.

Turn off chat for students


Apply Custom Message Policy using PowerShell

This needs to be run as a global admin. The variables at the top of the script should be changed to the AAD (or synced AD) group that you want to apply the messaging policy to and the message policy name.

#Variables to change - add the AAD group and custom message policy name here
$ADSecurityGroupNameToApplyPolicyTo="All Students"
$customMessagePolicyName="CustomStudentMessagingPolicy1"

# Install AzureAD PowerShell if you don't already have it - commented out below
# install-module azuread

#Import modules if you haven't already
Import-Module SkypeOnlineConnector
Import-Module AzureAD

#Connect to Skype and Azure AD
$userCredential = Get-Credential
$sfbSession = New-CsOnlineSession -Credential $userCredential
Import-PSSession $sfbSession
Connect-AzureAD -Credential $userCredential

$GroupUsers = Get-AzureADGroup -ALL $true -Filter "DisplayName eq '$ADSecurityGroupNameToApplyPolicyTo'" | Get-AzureADGroupMember -ALL $true | select mail
 
foreach ($GroupUser in $GroupUsers)
{
	$userEmail=$GroupUser.Mail
	write-host "Processing $userEmail"
	Grant-CsTeamsMessagingPolicy -PolicyName "$customMessagePolicyName" -Identity "$userEmail"
}

Calling Policies

Calling policies can be used to configure what can and can’t be done by users when calling on Teams. An example of this might be for preventing students from calling on Teams.

Calling policies can be found under Voice as shown below:

Calling Policies

These are the settings that can be applied:

Teams Calling Policy for Students

This is how we apply a calling policy:

#Variables to change - add the AAD group and custom message policy name here
$ADSecurityGroupNameToApplyPolicyTo="All Students"
$customMessagePolicyName="CallingPolicyForStudents"

# Install AzureAD PowerShell if you don't already have it - commented out below
# install-module azuread

#Import modules if you haven't already
Import-Module SkypeOnlineConnector
Import-Module AzureAD

#Connect to Skype and Azure AD
$userCredential = Get-Credential
$sfbSession = New-CsOnlineSession -Credential $userCredential
Import-PSSession $sfbSession
Connect-AzureAD -Credential $userCredential

$GroupUsers = Get-AzureADGroup -ALL $true -Filter "DisplayName eq '$ADSecurityGroupNameToApplyPolicyTo'" | Get-AzureADGroupMember -ALL $true | select mail
 
foreach ($GroupUser in $GroupUsers)
{
	$userEmail=$GroupUser.Mail
	write-host "Processing $userEmail"
	Grant-CsTeamsCallingPolicy -Identity "$userEmail" -PolicyName "$customMessagePolicyName"
}

Live Event Policies

Live Event policies might be used restricting who can attend or record them live events.

Live event policies can be found under Meetings as shown below:

Live Event Policies

These are the options when setting up a Live Events policy.

Teams Live Event Policy for Teachers

This is how we apply a Live Event policy:

#Variables to change - add the AAD group and custom message policy name here
$ADSecurityGroupNameToApplyPolicyTo="All Students"
$customMessagePolicyName="LiveEventPolicyForStudents"

# Install AzureAD PowerShell if you don't already have it - commented out below
# install-module azuread

#Import modules if you haven't already
Import-Module SkypeOnlineConnector
Import-Module AzureAD

#Connect to Skype and Azure AD
$userCredential = Get-Credential
$sfbSession = New-CsOnlineSession -Credential $userCredential
Import-PSSession $sfbSession
Connect-AzureAD -Credential $userCredential

$GroupUsers = Get-AzureADGroup -ALL $true -Filter "DisplayName eq '$ADSecurityGroupNameToApplyPolicyTo'" | Get-AzureADGroupMember -ALL $true | select mail
 
foreach ($GroupUser in $GroupUsers)
{
	$userEmail=$GroupUser.Mail
	write-host "Processing $userEmail"
	Grant-CsTeamsMeetingBroadcastPolicy -Identity "$userEmail" -PolicyName "$customMessagePolicyName"
}

Gaining access to OneDrives within your organisation

Below I have created a quick guide to show you how to gain access to a user’s OneDrive within your organisation. This video is for SharePoint administrators and you will need to be at least a SharePoint admin in Office 365 to carry out these steps.

A OneDrive site is effectively a SharePoint site collection with a document library. When a OneDrive is created by the user in Office 365, it grants the user site collection admin rights. It doesn’t add any other administrators or groups to the permissions. You can do this manually using the steps shown in the video below or you could create a script to apply permissions to all of your OneDrive sites using the PowerShell Client Object Model.



Using PowerShell to add a list or library WebPart to a SharePoint publishing page via CSOM

Thought I would share this as I struggled to find a complete article online how to do this. First of all, the code below is based on José Quinto’s post USING POWERSHELL TO ADD WEBPART TO SHAREPOINT PAGE VIA CSOM IN OFFICE 365. It’s a really good article on adding a content editor web part to a publishing page.

I couldn’t find any posts online on how to use the same technique to add a list view web part to a page. Eventually I figured out how to create the XML for adding a list view web part.

This is an adaptation of Jose’s function to add a web part to a page:

function AddWebPartToPage ($ctx, $sitesURL, $WebPartXml, $pageRelativeUrl, $wpZoneID, $wpZoneOrder) {
	try{
		Write-Host "Starting the Process to add the User WebPart to the Home Page" -ForegroundColor Yellow
		#Adding the reference to the client libraries. Here I'm executing this for a SharePoint Server (and I'm referencing it from the SharePoint ISAPI directory, 
		#but we could execute it from wherever we want, only need to copy the dlls and reference the path from here        
		Add-Type -Path "c:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\ISAPI\Microsoft.SharePoint.Client.dll" 
		Add-Type -Path "c:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\ISAPI\Microsoft.SharePoint.Client.Runtime.dll" 
		Write-Host "Getting the page with the webpart we are going to modify" -ForegroundColor Green
		#Using the params, build the page url
		$pageUrl = $sitesURL + $pageRelativeUrl
		Write-Host "Getting the page with the webpart we are going to modify: " $pageUrl -ForegroundColor Green
		#Getting the page using the GetFileByServerRelativeURL and do the Checkout
		#After that, we need to call the executeQuery to do the actions in the site
		$page = $ctx.Web.GetFileByServerRelativeUrl($pageUrl);
		$page.CheckOut()
		$ctx.ExecuteQuery()
		try{
		#Get the webpart manager from the page, to handle the webparts
		Write-Host "The page is checkout" -ForegroundColor Green
		$webpartManager = $page.GetLimitedWebPartManager([Microsoft.Sharepoint.Client.WebParts.PersonalizationScope]::Shared);
		Write-Host $WebPartXml.OuterXml
		#Load and execute the query to get the data in the webparts
		Write-Host "Getting the webparts from the page" -ForegroundColor Green
		$ctx.Load($webpartManager);
		$ctx.ExecuteQuery();
		#Import the webpart
		$wp = $webpartManager.ImportWebPart($WebPartXml.OuterXml)
		#Add the webpart to the page
		Write-Host "Add the webpart to the Page" -ForegroundColor Green
		$webPartToAdd = $webpartManager.AddWebPart($wp.WebPart, $wpZoneID, $wpZoneOrder)
		$ctx.Load($webPartToAdd);
		$ctx.ExecuteQuery()
		}
		catch{
			Write-Host "Errors found:`n$_" -ForegroundColor Red
		}
		finally{
			#CheckIn and Publish the Page
			Write-Host "Checkin and Publish the Page" -ForegroundColor Green
			$page.CheckIn("Add the User Profile WebPart", [Microsoft.SharePoint.Client.CheckinType]::MajorCheckIn)
			$page.Publish("Add the User Profile WebPart")
			$ctx.ExecuteQuery()
			Write-Host "The webpart has been added" -ForegroundColor Yellow 
		}	
	}
	catch{
		Write-Host "Errors found:`n$_" -ForegroundColor Red
	}
}

And this is the XML to add a SharePoint document library called “Documents” to the page.

$WebPartXml1 =  "
		<webParts>
			<webPart xmlns='http://schemas.microsoft.com/WebPart/v3'>
				<metaData>
				<type name='Microsoft.SharePoint.WebPartPages.XsltListViewWebPart, Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c' />
				<importErrorMessage>Cannot import this Web Part.</importErrorMessage>
			</metaData>
			<data>
				<properties>
					<property name='ListUrl' type='string'>Documents</property>
					<property name='ListName' type='string'>Documents</property>
				</properties>
			</data>
			</webPart>
		</webParts>"

The URL for a list is slightly different to a document library, the example below is the XML for an announcement list.

$WebPartXml1 =  "
		<webParts>
			<webPart xmlns='http://schemas.microsoft.com/WebPart/v3'>
				<metaData>
					<type name='Microsoft.SharePoint.WebPartPages.XsltListViewWebPart, Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c' />
					<importErrorMessage>Cannot import this Web Part.</importErrorMessage>
				</metaData>
				<data>
					<properties>
						<property name='ListUrl' type='string'>Lists/Student Announcements</property>
						<property name='ListName' type='string'>Student Announcements</property>
						<property name='JSLink' type='string'>~sitecollection/Style%20Library/cdb_custom_announcements/cdb_custom_announcements.js</property>
					</properties>
				</data>
			</webPart>
		</webParts>"

I then get the client context and pass the XML and variables to the function to add it to the page

$tenantAdmin = "user@domain.com"
$tenantAdminPassword = "password"
$secureAdminPassword = $(convertto-securestring $tenantAdminPassword -asplaintext -force)
$siteURL = "https://domain.com/sites/subsite";
$ctx = New-Object Microsoft.SharePoint.Client.ClientContext($siteUrl) 
$credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($tenantAdmin, $secureAdminPassword)  
$ctx.Credentials = $credentials
$relUrl = "/sites/subsite"
$pageRelativeUrl1 = "/Pages/Default.aspx"
$wpZoneID1 = "Top Left"
$wpZoneOrder1 = 0
#Run function
AddWebPartToPage $ctx $relUrl $WebPartXml1 $pageRelativeUrl1 $wpZoneID1 $wpZoneOrder1

Nice example of adding web parts to a SharePoint Online publishing page using PowerShell CSOM.

Adding SharePoint Online navigation from XML using PowerShell CSOM

The following PowerShell scripts were created to enable me to deploy a custom navigation across multiple site collections. You can use managed metadata navigation as mentioned in my previous post. Unfortunately this method doesn’t allow the user to reuse managed metadata navigation across multiple site collections (no idea why, I thought that was one of the advantages of managed metadata navigation!).

So a new and clean way of doing this is to use the CSOM for PowerShell. The code below deletes every navigation node using the first function and then adds each item added to an XML file. A strength of using this method is it can be manipulated to add additional logic for adding links to particular site collections depending on the variables in the XML file. Hope you find this useful.

For SharePoint design, workflows, automation, training and support please visit my SharePoint consultancy site www.clouddesignbox.co.uk. We offer education and business SharePoint solutions and services.

Deleting all navigation nodes using CSOM PowerShell

It’s fairly straightforward to enumerate nodes in an array, in this example I’m deleting all the top navigation menu nodes in a SharePoint site. This is how I would normally loop through the top navigation menu:

$topNav = $context.Web.Navigation.TopNavigationBar;
$context.Load($topNav);
foreach ($topNavItem in $topNav)
{
	Write-Host $topNavItem.Title
}

However if I want to loop through the menu and delete all the nodes, the above function errors as the array has changed each time it loops, the method below works but doesn’t catch all the menu items.

for ($ii = 0; $ii -lt $topNodes.Count; $ii++)
{
	Write-Host $topNodes[$ii].Title 
	$topNodes[$ii].deleteObject();
	$context.ExecuteQuery();
}

As we are enumerating the nodes, we are removing nodes from the start and changing the position of the other nodes in the array. As the loop continues to run, it can skip positions of some of the nodes.

A solution which works better is looping through the array backwards. As you loop through the array backwards, it doesn’t change the position of items still in the array.

for ($ii = $topNodes.Count - 1; $ii -ge 0; $ii--)
{
	Write-Host $topNodes[$ii].Title 
	$topNodes[$ii].deleteObject();
	$context.ExecuteQuery();
}

Hope you may find this useful, it can be difficult to find why the loop misses some random items and hopefully looping backwards will avoid any issues like this.

Setting up Azure Connect (DirSync) for Office 365

I’ve made a quick video guide on how to set-up Azure Connect (DirSync) to sync with Office 365. It’s done using a simple demo environment and I suspect that you may find complications and other errors when trying this out in a production live environment. Hopefully this is of use to you (even if it just makes it seem less scary!). I will try to get chance to write up this blog post in more detail rather than just the video at some point in the future.



Using PowerShell to find last DirSync

When troubleshooting DirSync issues with Office 365, it is sometimes difficult to know if the DirSync successfully applied to Office 365.

There is a quick way to check this and it provides the data for when the update last took place. First you will need to make sure that you have installed the Azure AD Module for PowerShell, you can download from here.

When this has installed, run Connect-MsolService to connect to the Office 365 tenancy, this should be a global administrator account.

Azure Connect PowerShell

Then run Get-MsolCompanyInformation to get the tenancy information which includes the last dirsync time and last password sync



Looping through all subsites in SharePoint Online

SharePoint Online requires use of the Client Object model when modifying the site via PowerShell due to there being no backend server exposed in Office 365. Users with previous experience of using JavaScript Client Object model will find this a familiar method.

In the example below, the Powershell iterates through all subsites (and subsites of subsites) in a site collection or root site. This could be used for disabling a web scoped feature or automating site modification.

First of all, the variables are initiated including the path to the relevant DLLs.

#connection variables and reference DLL
$username = "user@onmicrosoft.com"
$password = "Password123"
$SiteCollectionUrl = "https://tonyishere.sharepoint.com"
Add-Type -Path "c:\Microsoft.SharePoint.Client.dll"

A new object for the client context requires creating, this can be done in a reusable function as shown below.

# Generate ClientContext function so we can reuse
function GetClientContext($SiteCollectionUrl, $username, $password) {
     $securePassword = ConvertTo-SecureString $password -AsPlainText -Force
     $context = New-Object Microsoft.SharePoint.Client.ClientContext($SiteCollectionUrl) 
     $credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($username, $securePassword) 
     $context.Credentials = $credentials
     return $context
}

The following function has a nested call to call itself, this enables it to iterate through subsites of subsites. Without the nested function call, it would only cover a single subsite depth. The function writes the url of the site to the screen but this could be replaced with more complex functionality such as enabling a feature or creating a list item.

# function to loop through subsites
function catchsubsites ($subsiteurl){
	$clientContext = GetClientContext $subsiteurl $username $password
	$rootWeb = $clientContext.Web
	$childWebs = $rootWeb.Webs
	$clientContext.Load($rootWeb)
	$clientContext.Load($childWebs)
	$clientContext.ExecuteQuery()
	#do something on top level site
	write-host $rootWeb.url -ForegroundColor Yellow
	foreach ($childWeb in $childWebs)
	{
		#do something for each subsite
		write-host $childWeb.url -ForegroundColor Yellow
		#see if there are any subsites beneath this and loop all of them too
		catchsubsites $childWeb.url
	}
}

Finally the function is called to kick off the whole process of looping through the site.

#Finally run the function to get it all started!
catchsubsites $SiteCollectionUrl

This example could be refined so that it doesn’t authenticate at every request but it does show how simple it can be to create the complex PowerShell scripts that we are all used to using on SharePoint on-premises in the cloud version of Microsoft’s Office 365 SharePoint Online.

Update: Code updated 01/10/2016, fixed variable in loop and added section to run code for parent site

Setting the XSL Link in web part properties using PowerShell

webpartpropertiesTo change the XSL link value in the web part properties of a web part, it usually requires editing the page and entering the path to the XSL file in the web part properties (see image on right).

This can be automated using PowerShell. In the example below, the PowerShell loops through each web part on the page and sets the XSL Link value to “_layouts/xsl/tony.xsl”.

$spweb = Get-SPWeb “http://www.site.com”;
$url = $spweb.Url;
$WebPageUrl = “/Pages/test.aspx”
$spWpManager = $spweb.GetLimitedWebPartManager($WebPageUrl, [System.Web.UI.WebControls.WebParts
.PersonalizationScope]::Shared);
foreach($spwebpart in $spWpManager.Webparts)
{
$spwebpart.xsllink=”_layouts/xsl/tony.xsl”; #set the web part property
$spWpManager.SaveChanges($spwebpart);
}

The custom XSL style sheet in my previous post can be added to a large amount of lists that already exist by modifying this code and looping through all the sites/site collections. The code can also be modified to select web parts based on their title rather than changing the setting on every web part on the page.

Apply Custom Master Page to all Site Collections

I’ve just upgraded my MOSS 2007 site to SharePoint 2010. Oh No! The Master Page for 2007 doesn’t work in 2010 and I have 1500 Site Collections each with three websites!!

I need to activate my Master Page feature on each site collection and apply it to every site. Back in the old days, I would enumerate sites and create a huge script in excel to activate features and apply the Master Page. This is quite time consuming especially if you are upgrading different web applications every day. I thought it was time I tried to make this job easier and faster using PowerShell.

First start by looping though all the site collections in the web application then loop through the all the websites in the site collection (nested loop to get to every web in the web application). Then retrieve the URL of the web application and the URL of the site collection.
If the site is using a managed path for the site collections, it needs to be the relative path to the Master Page from the root of the web application.

e.g.

Web Application: http://www.tonyishere.co.uk
Site Collection: http://www.tonyishere.co.uk/sites/accounts
So the Master Page URL would be:  /sites/accounts/_catalogs/masterpages/custom.master

So how do we find the MasterPage URL path (as seen above) for each site collection? Using the length of the URL strings, the web application URL can be broken into a substring and appended to create the right path (see below).

e.g.
$len2 = $webname.length - 1
$len = $webappname.length
$len3 = $len2 - $len
$masterpath = $webUrl.substring($len,$len3)
$masterpath = $masterpath += "/_catalogs/masterpage/custom.master"

The new Master Page path then needs to be applied to the website.

$web.CustomMasterUrl = $masterpath

I found looping through 4500 websites in 1500 site collections took about 9 seconds, compared with the four hours that stsadm would of taken….big difference!!

Here is my full script to activate the feature on the site collection and loop into each website and apply the Master Page.


$webapp = Get-SPWebApplication https://www.tonyishere.co.uk
foreach ($s in $webapp.sites)
{
Enable-SPFeature MyCustomMasterPageFeature -Url $s.url
foreach ($web in $s.AllWebs)
{
$webUrl = $web.url
$web = Get-SPWeb $webUrl
$webappname = $webapp.url
$len2 = $webname.length - 1
$len = $webappname.length
$len3 = $len2 - $len
$masterpath = $webUrl.substring($len,$len3)
$masterpath = $masterpath += "/_catalogs/masterpage/custom.master"
$web.CustomMasterUrl = $masterpath
$web.MasterUrl = $masterpath
$web.Update()
}
}