Monthly Archives: March 2015

Script for assigning SharePoint Licenses to Office365

Adding SharePoint licenses to Office 365 can be a bit tricky.

If you add the E3 license, you get EVERYTHING that comes with E3, if that’s what you need, great, but what if you ONLY want SharePoint, and not Lync, email, etc??

I ran into this recently and used a few resources to come up with a script.

This article was really helpful:  http://www.powershellmagazine.com/2012/04/23/provisioning-and-licensing-office-365-accounts-with-powershell/

As was some script work by an awesome guy I work with named Chris.

The tricky thing here is you can’t directly grant just a SharePoint license in MSOL E3…

You have to do it subtractively.

Let me explain…..

Say you have 3 letters, A, B & C

You might expect to add a license for b like this:

Add-license -option B

It doesn’t work that way. (At least not in 2015 when I wrote this)

Instead you have to say:

Add-License -disable A C

No problem you say.

“I’ll just add code to disable A C”

That’s great, until….

Microsoft adds Option D

Now, when you try

Add-License -disable A C

You’ve just assigned a B and D license, when you only wanted to assign a B license.

Now you see the issue….

The solution is not too hard – we can pull a list of all options available, remove the one we want, and then build the disable list from that.

This way we won’t get caught when Microsoft springs options EFGHI on us.

The full script is below.

Note: there are some unused functions in the script for setting a password – if you have brand new users to Office 365, they may never have used their identity and will need the password reset and sent to them, if that’s the case, just add the call to Reset-Password -upn $upn at the appropriate place(s)

Here’s the script:

#http://sharepointjack.com
#based on content from http://www.powershellmagazine.com/2012/04/23/provisioning-and-licensing-office-365-accounts-with-powershell/
#use at your own risk, while this has worked for me, you should always test in a test environment and understand what it is the script is doing before using it.



write-host "Don't forget to connect to MSOL first!" -foregroundcolor yellow
Start-transcript 
function main()
{
    #variables used in the program
    $script:MissingUsers = @()
	$script:SMTPServer = "YOUR.SMTP.ADDRESS.COM"
	$importFilename = "_SPOUserList.txt"
	$LicenseType = "TENANTNAME:ENTERPRISEPACK"
	$SubLicense = "SHAREPOINTENTERPRISE"

	$path = $(get-location).path
	$userlist = get-content -path "$path\$importfilename"

	foreach ($upn in $userlist)
	{
	    	$upn = $upn.trim()
		
		#note the continue statement on next line, this skips the rest of the loop if the user is not found.
		if ((Check-UserExists -upn $upn) -ne "YES") {write-host "skipping $upn" -foregroundcolor black -backgroundcolor yellow; continue}
	    if ((CheckUserHasPrimaryLicense -upn $upn -PrimaryLicense $LicenseType) -eq $true)
		{
			#user has E3 license
			Write-host "User $upn has $LicenseType License, adding $SubLicense SubLicense"
			Add-SubLicense -upn $upn -PrimaryLicense $LicenseType -SubLicense $SubLicense
		} else {
		    #user has no license of any kind, but is still provisioned in MSOL
			write-host "User $upn does not have a license for $LicenseType adding now"
			Assign-NewLicense -upn $upn -Location "US" -PrimaryLicense $LicenseType -SubLicense $SubLicense
			
		}	
  #note, if you need to reset the users password and email that to them, add a line such as:
  # Reset-Password -upn $upn
	}	
    
	Report-MissingUsers   #report the names of any missing users so they can be investigated	
}#end main

##---------------------------------------------------------------------
##  Utility Functions from here down
##---------------------------------------------------------------------


##---------------------------------------------------------------------
## this function checks the upn (email address) to see if they exist at all in MSOL
## typically if they don't, there is a misspelling or other problem with the name and we want to report on that...
##---------------------------------------------------------------------
function Check-UserExists ($upn)
{
   $spouser = get-msoluser -user $upn -erroraction silentlycontinue
   if ($spouser -eq $null)
   {
       Write-host "user >$upn< Not found in MSOL" -foregroundcolor DarkRed -backgroundcolor Gray
	   $Script:Missingusers += $upn
       return $false 
   }
   else
   {
       return $true
   }
}

##---------------------------------------------------------------------
## this function checks the upn (email address) to see if it has the passed primary License (For example TENANTNAME:ENTERPRISEPACK)
## it returns true if the user has this license, and false if they do not.
##---------------------------------------------------------------------
function CheckUserHasPrimaryLicense ($upn, $PrimaryLicense) 
{
	$ReturnValue = $false
	$spouser = get-msoluser -user $upn
	$count = $($spouser.Licenses | where {$_.AccountSkuId -EQ $PrimaryLicense}).count
	write-host "Found exactly $count Licenses that matched $PrimaryLicense for user $upn" -foregroundcolor yellow
	if ($count -eq 1) 
	{ $ReturnValue = $true }
	return $ReturnValue
}

##---------------------------------------------------------------------
## this function Adds a given SubLicense (for example SHAREPOINTENTERPRISE) 
##  to a users Pre-Existing License (for example TENANTNAME:ENTERPRISEPACK)
##---------------------------------------------------------------------
function Add-SubLicense($upn, $PrimaryLicense, $SubLicense)
{
	$spouser = get-msoluser -user $upn
	#assemble a list of sub-licenses types the user has that are currently disabled, minus the one we're trying to add 
	$disabledServices = $spouser.Licenses.servicestatus | where {$_.ProvisioningStatus -eq "Disabled"}  | select -expand serviceplan | Select ServiceName | where {$_.ServiceName -ne $SubLicense}
	
	#disabled items need to be in an array form, next 2 lines build that...
	$disabled = @()
	foreach  ($item in $disabledServices.servicename) {$disabled += $item}
	
	write-host "  Adding Sub-license $SubLicense to existing $PrimaryLicense License to user $upn" -foregroundcolor green
	write-host "    Disabled License options: '$Disabled'" -foregroundColor green
	
	$LicenseOptions = New-MsolLicenseOptions -AccountSkuId $PrimaryLicense -DisabledPlans $disabled
	set-msoluserlicense  -userprincipalname $upn -licenseoptions  $LicenseOptions
}

##---------------------------------------------------------------------
## this function Assigns a new Primary License ($PrimaryLicense) and SubLicense to a users MSOL account
##---------------------------------------------------------------------
Function Assign-NewLicense($upn, $Location, $PrimaryLicense, $SubLicense)
{
    #assemble a list of sub-licenses available in the tenant, we want to disable all but our target sublicense
	$disabledServices = get-msolaccountsku | Where {$_.accountSkuID -eq $PrimaryLicense} | Select -expand "ServiceStatus" | select -expand "ServicePlan" | select ServiceName | where {$_.ServiceName -ne $SubLicense}
	
	#disabled items need to be in an array form, next 2 lines build that...
	$disabled = @()
	foreach  ($item in $disabledServices.servicename) {$disabled += $item}
	
	write-host "  Adding Completely new $PrimaryLicense license with $SubLicense sublicense for user $upn " -foregroundColor cyan
	write-host "    Disabled License options: $Disabled" -foregroundColor cyan
	
	$LicenseOptions = New-MsolLicenseOptions -AccountSkuId $PrimaryLicense -DisabledPlans $Disabled
	Set-MsolUser -UserPrincipalName $upn –UsageLocation $Location 
	Set-MsolUserLicense -User $Upn -AddLicenses $PrimaryLicense -LicenseOptions $LicenseOptions

}


##---------------------------------------------------------------------
## This function changes the MSOL users password and
## emails the user the temp password and some basic instructions
##---------------------------------------------------------------------
Function Reset-Password($upn)
{
	#generates a random password, 
	#Changes the MSOL Password,
	#emails the user the temp password and some basic instructions
	
	$tempPassword = Generate-Password
	Set-msolUserPassword -UserPrincipalName $upn -NewPassword $tempPassword
	$to = $upn
	$cc = "adminemail@yourdomain.com"
	$from = "adminemail@yourdomain.com"
	$Subject = "Important: Temporary password for your SharePoint Online Account"
	$body =  "Hello, <br/><br/>    You've just been granted a license for SharePoint Online.<br/><br/>"
	$body += "Your user ID is  <b>$upn</b> and your Temporary Password is <b>$TempPassword</b><br/>"
	
	$body += "Please log on to <a href='http://portal.office.com'>http://portal.office.com</a> <b>right now</b> and change the temporary password above to one you'll remember.<br/><br/>"
	
	write-host "Sending email"
	Send-MailMessage -From $from -to $to -cc $cc -Subject $subject -bodyashtml $body -SmtpServer $script:SMTPServer	
	Write-host "Email sent to $upn with $tempPassword"
}

##---------------------------------------------------------------------
## This function generates a random password
## 17 chars long with guaranteed min of 1 number, 1 lower and 1 upper case 
##---------------------------------------------------------------------
Function generate-Password
{
	$alphabetUpper = $NULL;
	for ($a=65; $a -le 90; $a++)
	{
		$alphabetUpper+=,[char][byte]$a 
	}

	$alphabetlower = $NULL;
	for ($a=97; $a -le 122; $a++)
	{
		$alphabetlower+=,[char][byte]$a 
	}

	$ascii=$NULL;
	For ($a=48;$a -le 122;$a++) 
	{
		$ascii+=,[char][byte]$a
	}

	$Fullset=$null
	For ($a=48;$a -le 57;$a++)  #0-9
	{
		$Fullset+=,[char][byte]$a
	}
	For ($a=65;$a -le 90;$a++) #A-Z
	{
		$Fullset+=,[char][byte]$a
	}
	For ($a=97;$a -le 122;$a++) #a-z
	{
		$Fullset+=,[char][byte]$a
	}

	$onepassword = $null
     #start password with an alphabetical letter.
	 $onepassword += (get-random -InputObject $alphabetlower)
	 $onepassword += (get-random -InputObject $alphabetUpper)
	 #now add a number to guarantee we have a number
	 $onepassword += get-random -Minimum 0 -Maximum 9
	 
	 
     #now add 14 random chars from the combined set.
	 for ($pwlen=0; $pwlen -le 11; $pwlen++)
	 {
       $onepassword += (get-random -inputObject $Fullset)
     }
	 return $onePassword
}

##---------------------------------------------------------------------
## This function displays/emails any missing user infomation
##---------------------------------------------------------------------	 
function Report-MissingUsers()
{
  if ($script:MissingUsers.length -gt 0)
  {
    write-host " -------------------------------------- "
	Write-host "          The following users were not found in SPO:          " -foregroundcolor magenta -backgroundcolor gray
	$script:Missingusers
	Write-host "          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^          " -foregroundcolor magenta -backgroundcolor gray
	
	$to = "adminemail@yourdomain.com"
	$from = "adminemail@yourdomain.com"
	$Subject = "Missing users in last license assignment"
	$body = $script:MissingUsers -join "<br/>"
	
	write-host "Sending email of missing users to $to"
	Send-MailMessage -From $from -to $to -Subject $subject -bodyashtml $body -SmtpServer $script:SMTPServer	
	Write-host "Missing user email sent to $to"
  }
}

main #call main procedure, this line must be the very last line in the file.
Stop-transcript #ok, actually this line must be the very last line...

 

Create a ShareGate User mapping file between on Premise AD and o365 / Azure AD

We use ShareGate to migrate content.

We recently started using ShareGate to migrate content from On Premise to SharePoint Online.

When I did this, I found that one of our domain’s users kept showing up as errors in ShareGate – it said it could not find the user in SharePoint Online.

ShareGate has a nice feature for mapping users from one system to users in another – but doing this manually to any scale would be pretty time consuming.

Thankfully, ShareGate lets us save the mappings, which are just XML files with a .sgum file extension.

Wouldn’t it be great if there was a way to automate creating a mapping file like this for everyone in the domain at once?

Have a look at the script below, it pulls all the user accounts from an OU in AD, then looks up each user to find them in MSOL (Office 365 Azure AD) Then grabs the o365 display name and makes the mapping . Any user not found is logged so it can be dealt with separately.

The whole thing is written out as a complete .sgum file, ready to import into ShareGate the next time you migrate!

Note I didn’t figure out the XML stuff in a vacuum – I found an article on powershellmagazine.com to be very helpful and noted it in the script.

# sharepointjack.com
# use at your own risk

$users = get-aduser -server server.domain.com -filter * -searchbase "OU=Users,DC=server,DC=domain,DC=COM"

$total = $users.count
$count = 0
$badnames = @()


#--------------------------------------
# from http://www.powershellmagazine.com/2013/08/19/mastering-everyday-xml-tasks-in-powershell/
$dt = get-date -format "yyyyMMdd"
$path = "$(get-location)\UserMap_$dt.sgum"
$XmlWriter = new-object System.XML.XMLTextWriter($path, $null)
$XmlWriter.Formatting = 'Indented'
$xmlwriter.Indentation = 1
$XmlWriter.IndentChar = "`t"


#write the header
$xmlWriter.WriteStartDocument()

$XmlWriter.WriteComment("Start of XML")
$XMLWriter.WriteStartElement('UserAndGroupMappings')
$XmlWriter.WriteAttributeString('xmlns:xsd', 'http://www.w3.org/2001/XMLSchema')
$XmlWriter.WriteAttributeString('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance')


$XMLWriter.WriteStartElement("Mappings")
$XmlWriter.WriteComment("Start of main loop")
foreach ($OneUser in $users)
{ 
    $count ++
    $XMLWriter.WriteStartElement("Mapping")
    $SourceAccountName = "DOMAINGOESHERE\$($OneUser.Samaccountname)"
    $SourceDisplayname = $OneUser.name

    $DestinationAccountName = "i:0#f|membership|$($OneUser.UserPrincipalName)"
    #pull the destination user name from MSOnLine
    $DDN = $(get-MSOLuser -userprincipalName $OneUser.UserPrincipalName).Displayname
    #if MSOL not found, length will be zero. in that case use the AD displayname
    if ($DDN.length -eq 0)
    {
       $DestinationDisplayname = $OneUser.name
       $badnames += $OneUser.userprincipalName
       write-host "Warning: $($OneUser.userprincipalName) username Not found in MSOL" -foregroundcolor cyan
    }
    else
    {
       $DestinationDisplayname = $DDN 
       write-host "$count of $total"
    }
 
    $XMLWriter.WriteStartElement("Source")
    $XmlWriter.WriteAttributeString('AccountName', $SourceAccountName)
    $XmlWriter.WriteAttributeString('DisplayName', $SourceDisplayname)
    $XmlWriter.WriteEndElement() #source

    $XMLWriter.WriteStartElement("Destination")
    $XmlWriter.WriteAttributeString('AccountName', $DestinationAccountName)
    $XmlWriter.WriteAttributeString('DisplayName', $DestinationDisplayname)
    $XmlWriter.WriteEndElement() #Destination
    $XmlWriter.WriteEndElement() #mapping
}
$XmlWriter.WriteEndElement() #mappings
$XmlWriter.WriteEndElement() #UserAndGroupMappings


#finalize the document
$xmlWriter.WriteEndDocument()
$xmlWriter.Flush()
$xmlWriter.Close()

$bnpath = "$(get-location)\BadNames_$dt.txt"
$badnames | out-file -filepath $bnpath
notepad $path

– Jack

Add a person as a site collection administrator to every Office 365 Site / SharePoint Online Site Collection

The Problem:

In SharePoint online (at least as of early 2015) site collection administrators have to be granted on a site by site basis.

When you create a new site collection using  https://yoururl-admin.sharepoint.com, you are only allowed to pick ONE administrator for the Site collection (In on premise, you used to pick two)

NewSiteCollectionSPOdialog

Now a little trick you can use is, after the site collection is created, you can check the site collection then click the “owners” tab:

SPOadminBar

and from that screen you can add as many site collection administrators as you’d like:

AddSPOadminDialog

 

But there is a downside, you can’t “select all” on all your site collections and add a user to all site collections at once.

Now, I hear you saying “Jack: What if I have 500 site collections and we add a new member to our team?” There’s got to be a better way, right? And it turns out, there is.

The Solution: PowerShell…

A Quick note before we get to the script: You’ll need the SharePoint Online Management Shell installed on your PC before this will work.
Here’s a quick overview of how to use the script:

Update all the relevant variables:

  1. Admin site URL ($adminurl), and the $username that has permissions to log into the admin site url to make the change.
  2. put in your $tenantURL
  3. Update the list of $SiteCollectionAdmins with the list of users you want to make site collection admins

Run the script.

When you run the script it will try to logon to your SPO account and will prompt you for your SPO password, then you should see some slow and steady progress as it runs through each site collection. Finally, at the end you can review the log file to see if there were any issues.

The Script:

# Jack Fruh - sharepointjack.com
# add a user or users to the site collection admin role on every site collection in Office 365 sites (SharePoint Online)

#setup a log path
$path = "$($(get-location).path)\LogFile.txt"
#note we're using start-transcript, this does not work from inside the powershell ISE, only the command prompt

start-transcript -path $Path
write-host "This will connect to SharePoint Online"

#Admin Variables:
$Adminurl = "https://yoururl-admin.sharepoint.com"
$username = "your@email.com"

#Tenant Variables:
$TenantURL = "https://yoururl.sharepoint.com"

$SiteCollectionAdmins = @("firstuser@yourdomain.com", "seconduser@yourdomain.com", "etc@yourdomain.com")

#Connect to SPO
$SecurePWD = read-host -assecurestring "Enter Password for $username"
$credential = new-object -typename System.Management.Automation.PSCredential -argumentlist $username, $SecurePWD

Connect-SPOService -url $Adminurl -credential $credential
write-host "Connected" -foregroundcolor green


$sites = get-sposite
Foreach ($site in $sites)
{
    Write-host "Adding users to $($site.URL)" -foregroundcolor yellow
	#get the owner group name
	$ownerGroup = get-spoSitegroup -site $site.url | where {$_.title -like "*Owners"}
	$ownertitle = $ownerGroup.title
	Write-host "Owner Group is named > $ownertitle > " -foregroundcolor cyan
	
	#add the Site Collection Admin to the site in the owners group
	foreach ($user in $SiteCollectionAdmins)
	{
		Write-host "Adding $user to $($site.URL) as a user..."
		add-SPOuser  -site $site.url -LoginName $user -group $ownerTitle
		write-host "Done"
		
		#Set the site collection admin flag for the Site collection admin
		write-host "Setting up $user as a site collection admin on $($site.url)..."
		set-spouser -site $site.url -loginname $user -IsSiteCollectionAdmin $true
		write-host "Done"	-foregroundcolor green
	}
}
Write-host "Done with everything" -foregroundcolor green 
stop-transcript