Administering Sharepoint – PowerShell is your friend

At Christian Aid we recently upgraded from Sharepoint 2003 to Microsoft Office Sharepoint Server 2007 (commonly abbreviated to MOSS 2007) which has definitely meant improvements in a number of areas, but administration is still a royal pain in the ass.  Some of this is down to the rather busy admin web pages (here I think the Rule of 7 actually would have been a useful principle for Microsoft to follow), and some down to the information architecture we rolled out in Sharepoint 2003.

Sharepoint Architecture

Sharepoint is organised into the following key objects:

Sharepoint Objects
Sharepoint Objects

At Christian Aid we have opted to create a new Site Collection for each Team Site – the team site is effectively the root and only site in a Site Collection.  We are beginning to see a number of problems with this model.  Before I get into those, let me explain the reasons why we originally chose this path.

I think that Microsoft’s intention has from the out been that when creating a Sharepoint Farm, the administrators create a number of site collections, each storing their content in a Content Database.  All team sites would then be created under this limited number of Site Collections.  This model allows for a certain degree of granularity in backup/restore.  I say a certain degree because since your content is stored in a database, when restoring a lost file it is necessary to restore the whole content database.  If you have a single content database, this may mean restoring 100+ gigabytes in order to get a 2kb file!  This is one of the reasons that Christian Aid opted to set up a Site Collection for each team site, the idea being that you could have a larger number of smaller content databases making the restore process less of a headache.

Another reason for our Site Collection = Team Site model is that permissions in Sharepoint are inherited from the Site Collection.  In Sharepoint 2003, if you wanted to have a different set of Permission Groups or Permission Levels for different team sites, one way of achieving this was to use different site collections.

Inheritance problems

It is this inheritance issue that is now biting us in the ass.  We have 160+ site collections, and since this is the level from which permissions are inherited, if we want to make a change across the board there are 160+ places to make the change.  No small problem.

The problem is compounded because, in my thinking, Sharepoint actually has two security models.  One is geared towards Administration of the Farm, the other more towards User level access control, including administration of sites.  The Site Collection object kind of sits in a grey area  between the two.  Here is my understanding of security in Sharepoint 2007:

Sharepoint Security Model
Sharepoint Security Model

Complications in the Site Collection permissions are that the Site Collection administrators cannot be AD or Sharepoint Groups.  AD Users only.  Why this is the case I don’t know.  If you want to have centralised admin, and your staff changes then you will have to edit the Site Collection Administrators in all of your Site Collections.  Ouch!  It would be so much nicer to just add/remove users from an AD group.

Choices

Given our Sharepoint install is so large (over a terabyte of data, over a million documents), it isn’t a simple matter to rearchitect it.  Many readers will point out that the upgrade was the perfect opportunity to do this – however for a few reasons the organisation wasn’t ready for big changes.

The next option is to find a way of centrally administering all these Site Collections.  There are a number of admin tools out there – Quest’s Sharepoint Administrator and Avepoint spring to mind.  However these tools are pricey, and you may not use the functionality frequently enough to justify the cost.

It is also possible to programme SharePoint using C#.  For me this would mean a string of learning opportunities.  I want something cheap and relatively quick.

PowerShell and stsadmn to the rescue

Microsoft’s new PowerShell is able to interact with the Sharepoint Object API and the scripting language is relatively simple, especially if you have ever done any *nix shell scripting. So after reading a few extremely handy blog entries – (links at end of post) I settled down to write some PowerShell scripts to centrally make changes to our Site Collection permissions.

The first issue I hit was the complexity of the Sharepoint Object Model.  The naming convention runs a bit contrary to Sharepoint’s nomenclature – a Site Collection is of the SPSite object, whereas a Site is a SPWeb.  Some objects are tied to the Microsoft.SharePoint namespace, while others are part of Microsoft.Office.Server namespace which is documented in a completely different place!  It is possible to figure all this out, but it is a bit of a Choose Your Own Adventure experience, flicking between the different objects documentation on MSDN and trying to get a grip on how it all relates.  I found printing out key pages very useful!

One epiphany for me was that it was possible to call stsadmn commands from PowerShell.  Instead of trying to figure out how to set a Site Collection Owner via the object model, you can instead use PowerShell to iterate through SharePoint object collections (i.e. Site Collection, Web Applications, Sites etc.) and make changes using stsadmn.

So after all that waffling, here are the scripts.

Requirements

The following scripts should be installed to a server with SharePoint installed, and ran as the SharePoint system user – I’m not clear that this last bit is always necessary, but it was the only way I could get these things to run.  Use Run As to open PowerShell as the System User and you are good to go.

Supporting functions

The following script contains functions that will be used by the other scripts.  It also calls the assemblies required for PowerShell to interact with the SharePoint object model: (Download SPFunctions)

# Load SharePoint assemblies
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint")
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Office.Server")

#####################################################################################
#
# Sharepoint basic functions
#
#####################################################################################

#######
#
#	Get-SPFarm
#
#	Gets the Farm on this server
#
#######

function global:Get-SPFarm{
	return [Microsoft.SharePoint.Administration.SPFarm]::Local
}

#######
#
#	Get-SPWebApps
#
#	Gets all the web apps on a farm
#
#######

function global:Get-SPWebApps{
	Get-SPFarm |% {$_.Services} | where {'$_.TYPEName -eq "Windows Sharepoint Services Web Application"'} |% {$_.WebApplications} |% {Write-Output $_}
}

#######
#
#	List-SPWebApp-GUID
#
#	Lists the Web Applications on a farm, and their GUIDS
#	(which can be used to grab a single Web App).
#
#######
function global:List-SPWebApp-GUID {
	Get-SPWebApplication | Select Id, DisplayName | Format-Table -AutoSize
}

#######
#
#	Get-SPWebApp
#
#	Gets a specific web app by its GUID
#	Hint: use List-SPWebApp-GUID to list all the GUIDs
#
#######
function global:Get-SPWebApp($guid){
	# If no guid sent get all the web applications for the farm
	if($guid -eq $null){
		Get-SPFarm |% {$_.Services} | where {'$_.TYPEName -eq "Windows Sharepoint Services Web Application"'} |% {$_.WebApplications} |% {Write-Output $_}
	} else {
		$myfarm = Get-SPFarm;
		return $myfarm.GetObject($guid);
	}
}

########
#
#	Get-SPWeb
#
#	Gets a single site by its URL
#
########
function global:Get-SPWeb($url,$site) {
	if($site -ne $null -and $url -ne $null){"Url OR Site can be given"; return}
	#if SPSite is not given, we have to get it...
	if($site -eq $null){
		$site = Get-SPSite($url);
	}
	#Output 1 or more sites...
	if($url -eq $null){
		for($i=0; $i -lt $s.AllWebs.Count;$i++){
			Write-Output $s.AllWebs[$i]; ##Send through Pipeline
			$s.Dispose(); ##ENFORCED DISPOSAL!!!
		}
	}else{
    	Write-Output $site.OpenWeb()
	}
}

########
#
#	Check-GUID
#
#	Checks that a given string is the format of a GUID (but doesn't verify that the guid is valid)
#
#########
function global:Check-GUID($guid){
	$regex = "^({{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}}{0,1})$";
	if ($guid -match $regex){
		return $true;
	} else {
		return $false;
	}
}

########
#
#	Get-SPSite
#
#	Gets a single Site Collection by its URL
#
########
function global:Get-SPSite($url){
	return New-Object Microsoft.SharePoint.SPSite($url);
}

########
#
#	Get-UserProfileConfigManager
#
#	Returns UserProfileConfigManager object used for managing MOSS User Profiles
#
########
function global:Get-UserProfileConfigManager([string]$PortalURL){
	# Get portal context object
	$site = Get-SPSite($PortalURL);
	$servercontext = [Microsoft.Office.Server.ServerContext]::GetContext($site);
	$site.Dispose();

	# Return the UserProfileConfigManager
	New-Object Microsoft.Office.Server.UserProfiles.UserProfileConfigmanager($servercontext);
}

########
#
#	Get-MemberGroupManager
#
#	Gets the MemberGroupManager object for a portal - this can be used to make changes to membership groups
#
########
function global:Get-MemberGroupManager($PortalURL){
	$ugm = Get-UserProfileManager($PortalURL);$m
	$ugm.GetMemberGroups();
}

########
#
#	Get-UserProfileManager
#
#	Gets a UserProfileManager object for a specific portal.  This can be used to make changes to
#	user profiles
#
########
function global:Get-UserProfileManager($PortalURL){
	# Get portal context object
	$site = Get-SPSite($PortalURL);
	$servercontext = [Microsoft.Office.Server.ServerContext]::GetContext($site);
	$site.Dispose();

	# Return the UserProfileManager object
	New-Object Microsoft.Office.Server.UserProfiles.UserProfileManager($servercontext);
}

########
#
#	Delete-MemberGroup
#
#	Removes a MemberGroup from the given Portal URL
#
########
function global:Delete-MemberGroup($PortalURL){
	$mm = Get-MemberGroupManager($PortalURL);
	$name = Read-Host "MailNickName";
	foreach ($m in $mm){
		if ($m.MailNickName -eq $name){
			Write-Host "Deleting " $m.MailNickName;
			$m.Delete();
		}
	}
}

These functions are mainly based on those described on Zach Rosenfield’s blog.  Any errors are obviously mine.  Save this as SPFunctions.ps1.

Update Owner and Secondary Owner of Site Collection

This script zings through all the site collections on the selected farm, and updates the Owner and/or the Secondary owner.  Note that it calls the SPFunctions.ps1 file above. (Download edit-spowners)

#######
#
#	edit-spowners
#
#	Sets the owner and secondary login for all site collections in a web application
#
#	TODO: Check userinput and handle errors gracefully
#
#######

.SPFunctions.ps1;

####################################################################################
#
#	Main script
#
####################################################################################

# Preamble
cls;
write-host "###################################################################################

This application updates the owner and secondary owner of all site collections for
the given Web Application.

Here is a list of all Web Applications on the farm" -foregroundcolor yellow;

# List the GUIDs of all web apps on this farm
List-SPWebApp-GUID;

write-host "Please copy the GUID for the web application you wish to use by selecting then
clicking the right mouse button."  -foregroundcolor yellow;

write-host "
Note that the Central Admin site collection has no name,
but does have a GUID!  Don't select this one by accident!
" -foregroundcolor red;

Write-Host "You can paste the GUID by clicking the Right mouse button again" -ForegroundColor yellow;
# Prompt user for GUID to use
$guid = Read-Host -Prompt "GUID";
$guid = $guid.Trim();

# Check GUID for correct format
Write-Host "Checking GUID";
switch(Check-GUID($guid)){
	"False" { Write-Host "Error: Invalid GUID.  Exiting..." -ForegroundColor red; return;}
}

# Get the specified web app
$webapp = Get-SPWebApp($guid);

# Check that webapp has been got and if not inform user
Write-Host "The following Web App will be affected: " $webapp.name;

# List out the site collections that will be changed and prompt to continue
Write-Host "The following site collections will be affected" -ForegroundColor yellow;
$webapp.Sites | ft url, owner, secondarycontact;

$continue = Read-Host -Prompt "Continue? (y|n)>";
switch($continue){
	"y" {break;}
	default {Write-Host "Exiting..." -ForegroundColor red; return;}
}

# Ask user to specify username for owner
$owner = Read-Host -Prompt "Username of new Owner
1) Use DOMAINusername format to set a new Owner
2) Leave blank to leave the Owner alone
Owner> ";

# Note STSADM checks validity of usernames

# Ask user to specify username for secondary owner
$second = Read-Host -Prompt "Username of new Secondary Contact
1) Use DOMAINusername format to set a new Secondary Contact
2) Type none to set the Secondary Contact as blank
3) Leave blank to leave the Secondary Contact alone
Secondary Contact> ";

# Check username is correct format or empty string

# Create the appropriate STADM option to set the owner and/or secondary contact for each site
switch($owner){
	"" {$own_param = ''}
	default {$own_param = "-ownerlogin `"$owner`""}
}

switch($second){
	"" {$sec_param = ''}
	"none" {$sec_param = "-secondarylogin `"`""}
	default {$sec_param = "-secondarylogin `"$second`""}
}

# For each site collection echo the url and make the update (will generate success message)
$time = Measure-Command {
	foreach ($site in $webapp.Sites) {
		$command = "stsadm -o siteowner -url `"" + $site.url + "`" $own_param $sec_param";
		Write-Host $command;
		Invoke-Expression $command;
		$count++;
	}
}

Write-Host "$count Site Collections updated in" $time.TotalSeconds "seconds." -ForegroundColor yellow;

##############################      End of script     #############################

This makes good use of the stsadmn trick – note you have to build your command as a string then call it using Invoke-Expression.

Adding/removing users from Site Collection Administrators

I’ll be using a similar technique to create a script to add and remove users from the Site Collection Administrators list on each site.  Watch this space.

Handy links

10 thoughts on “Administering Sharepoint – PowerShell is your friend

  1. how do I run this script?
    the update-spadmins to update the site collection administrators. thanks

  2. i receive the following error
    The term ‘Get-SPWebApplication’ is not recognized as a cmdlet, function, operab
    le program, or script file. Verify the term and try again.
    At C:powershellscriptsSPSiteCollectAdmin.ps1:27 char:21
    + Get-SPWebApplication <<<< | ft name, id;
    when i run powershell.exe c:powershellscriptsSPSiteCollectAdmin.ps1 when i’m in c:

    any ideas?
    Thanks

    • Have you saved the preceding script in the same directory?

      I’ve made some minor changes to the scripts – but these were to make the scripts a little clearer, and wouldn’t change your case.

      To test the function in question, run the SPFunction.ps1 script, then try Get-SPWebApplications | ft name, id

      If that returns a table with the name and GUID of each Web Application, then the script should work for you.

  3. Thanks for the response 🙂
    Yes, they are both in the same directory.
    C:PowerShellScriptsSPFunctions.ps1
    C:PowerShellScriptsSPSiteCollectAdmin.ps1

    PS C:> powershell.exe c:powershellscriptsSPFunctions.ps1

    An empty pipe element is not permitted.
    At C:powershellscriptsSPFunctions.ps1:35 char:27
    + |% <<<

    An empty pipe element?

  4. I’ve updated both posts with powershell scripts to include downloadable ps1 files – I think the rendering of the pages might have corrupted some of the scripts so they don’t work correctly. Using the downloaded files should work better.

  5. Hi Ducan,

    No doubt your block is very helpful.
    Can you please post a scipt to create a new webapplication & a new site collection in MOSS 2007 using powershell?
    I have gone through a number of blogs but didn’t find any helpful information.

Leave a comment