Category Archives: Sharepoint

Information on the configuration and maintenance of Sharepoint Services

Discover user rights in SharePoint

Among the top items capable of derailing your whole day or week are requests from auditors.  Who has access to a resource?  When did they exercise those rights?  In the pas few months, I have had several requests of this sort related to SharePoint rights.  Since I have once again started working on our SharePoint 2010-to-2013 migration project, and most of the SharePoint Powershell cmdlets were fresh in my mind, I though I would take a crack at this somewhat intimidating task.

As usual, writing a useful script took more time that I would have liked, but I am fairly pleased with the results.  The final product makes heavy use of Regular Expressions.  Special thanks go out to RegEx Hero, an online .NET regular expressions tester:
http://regexhero.net/tester/
AND, of course, to the Regular-Expressions.info site:
http://www.regular-expressions.info/

Using .NET-style RegEx named capture groups, I was able to eliminate redundant loops though the SharePoint web site list, thus making it possible to crawl all SharePoint web and site-level ACLs in only a few minutes. Hurray!

This code will work only on SharePoint 2010 farms that use Windows authentication. There may be limitations related to sites with multiple Windows domains as well. I will need to update this script in the near future to handle claims authentication, but we will cross that bridge when we come to it.

The script has some pretty convoluted loops that may not make any intuitive sense… I have tried to insert comments to explain what is going on. If you do choose to use this script in your environment and find it difficult to understand, feel free to contact me with questions.

I think that the script has been posted without wordpress-induced errors. The main problem there is the use of .NET style named capture groups, which use ‘greater than’/’less than’ characters. WordPress hates gt/lt, which resemble XML/HTML/XHTML tags. I had to enter these bits of code as HTML escape sequences instead of raw code. It looks like it worked this time.

#findUserPermissions.ps1
#J. Greg Mackinnon, 2015-03-25
#
#Recurses though the web application provided in -webAppplication for the users (samaccountname format) provided in -Users,
#for the domain provided in -Domain.
#
# Requires: Microsoft.SharePoint.PowerShell PSSnapin
#          ActiveDirectory PowerShell module
#
# Provides: Comma-separated value file with user, permission, and site data for all discovered permissions, 
#          at path provided in the -logPath parameter.          
[cmdletBinding()]
param(
    [parameter(
        Mandatory=$true,
        HelpMessage='Enter a username or comma-separated list of usernames in samAccountName format.')]
        [ValidatePattern('^\b[\w\.-]{1,20}\b$')]
        [string[]]$users,
    [parameter()]
        [ValidatePattern('\b[\w\.-]{1,15}\b')]
        [string]$domain = 'CAMPUS',
    [parameter(
        HelpMessage='Enter the URL of the SharePoint web application for which all webs will be searched.')]
        [ValidatePattern('^http[s]*://\w+\.\w+')]
        [string]$webApplication,
    [parameter(
        HelpMessage='Enter the URL of the single SharePoint site for which all subwebs will be searched.')]
        [ValidatePattern('^http[s]*://\w+\.\w+')]
        [string]$spSite,
    [string]$logPath = 'c:\local\temp\findUserPermissions.log'
)
Set-PSDebug -Strict

function getPermType($mask) {
    #SharePoint permissions/roles are exposed programatically as "PermissionMask" attribute of the SPWeb.Permissions.Member objects.
    #Most of these masks are numeric.  This function will convert the numeric value into a "friendly" string, derived by examining 
    #site permissions in a web browser:
    [string]$return = switch ($mask) {
        'FullMask'   {[string]'FullControl'}
        '1012866047' {[string]'Design'}
        '1011028719' {[string]'Contribute'}
        '138612833'  {[string]'Read'}
        '138612801'  {[string]'ViewOnly'}
        '134287360'  {[string]'List-Library:UnknownAccess'} #Note that this mask has the label "Limited Access" in the GUI.
        default      {[string]"Unknown:$mask"}
    }
    return $return
}

function checkADGroupMembers {
    param ([string]$adGroupName,[string]$adGroupSid,[string]$userRegex)
    #Searches the AD Group provided in -adGroup for matches against the regex provided in -userRegex.
    #Search is performed against the domain of the computer running the script.
    #The userRegex value can be generated using the "regexifyDomainUser" function. It MUST capture the 
    # username/samAccountName to a named capture group called 'name'.
    #Returns boolean true/false.
    #Need to add ability to return RegEx match objects from the search.
    #Optional ability to specify the domain to search?
    
    [Int32]$i = $adGroupName.IndexOf('\')
    [string]$groupSam = $adGroupName.Substring($i + 1)
    #write-host "Getting members of:" $groupSam
    
    [String[]]$returns = @()
    
    #Domain Users could take a long time to process, so let's just assume that the user is a domain user:
    if (($groupSam -eq 'domain users') -or ($groupSam -eq 'authenticated users')) {
        $returns += "!AllUsers!"
        return $returns
        break
    }
    #Get-ADGroupMember will error frequently because SharePoint contains a lot of orphaned groups.
    #Use "ErrorAction Stop" for force an breaking error when this happens, and just set $match to $false/
    try {
        [array]$grpMembers = @()
        if ($adGroupSid) {
            $grpMembers += Get-ADGroupMember -Recursive -Identity $adGroupSid -ErrorAction Stop `
                | Select-Object -ExpandProperty SamAccountName 
        } else {
            $grpMembers += Get-ADGroupMember -Recursive -Identity $groupSam -ErrorAction Stop `
                | Select-Object -ExpandProperty SamAccountName 
        }
    } catch {
        write-host "    AD group $groupSam does not exist" -ForegroundColor Red
        $returns = $null
        break
    }
    if ($grpMembers.count -gt 0) {
        foreach ($memb in $grpMembers) {
            #write-host "testing" $memb "against" $userregex
            if ($memb -match $userRegex) {
                #write-host "regexmatch found: " $matches.user
                #Return the current regex named group "user".  THis is just showing off...
                #I could simply skip the capture groups and just return $memb.
                $returns += $matches.user
            }
        }
    } else {
        write-host "    AD group $groupSam has no members" -ForegroundColor Red
    }
    if ($returns.count -gt 0) {
        return $returns
    } else {
        return $null
    }
}

function regexifyDomainUser {
    param ([string]$user,[string]$domain)
    #Converts the provided domain\username pair into a regex that can 
    #be used to search for the same pattern in a larger string.
    #This regex will return the username in a capture group named "user".
    if ($user -match '\.') {
        $userMatch = $user.Replace('.','\.')
    } else {
        $userMatch = $user
    }
    [string]$regexUser = '^' + $domain + '\\(?<user>' + $userMatch + ')$'
    return $regexUser
}
function regexifyUser {
    #Converts the provided username into a regex that can 
    #be used to search for the same pattern in a larger string.
    #This regex will return the username in a capture group named "user".
    param ([string]$user)
    if ($user -match '\.') {
        $userMatch = $user.Replace('.','\.')
    } else {
        $userMatch = $user
    }
    [string]$regexUser = '^(?<user>' + $userMatch + ')$'
    return $regexUser
}

if ((Get-PSSnapin -Name Microsoft.SharePoint.PowerShell -ErrorAction SilentlyContinue) -eq $null) {
    Add-PSSnapin Microsoft.SharePoint.PowerShell
}
Import-Module ActiveDirectory

#Set up regex strings that contain all of the samAccountNames provided in the input parameters.
#We want to be able to match all possible users with one regex for processing efficiency.

#DomainUsers will match against DOMAIN\username.
[string]$regexDomainUsers = regexifyDomainUser -user $users[0] -domain $domain
#Users will match againstjust username.
[string]$regexUsers = regexifyUser -user $users[0]
if ($users.count -gt 1) {
    for ($i=1; $i -lt $users.count; $i++) {
        #Set up a regex for matching the user in domain\username format:
        [string]$regexDomainUser = '|' + $(regexifyDomainUser -user $users[$i] -domain $domain)
        $regexDomainUsers += $regexDomainUser
        [string]$regexUser = '|' + $(regexifyUser -user $users[$i])
        $regexUsers += $regexUser
    }
}

## Initialize output log:
if (Test-Path -LiteralPath $logPath) {
    Remove-Item -LiteralPath $logPath -Force
}
#CSV header row:
[string]$out = "user,Role,WebUrl,aceDetails"
$out | Out-File -FilePath $logPath 

#Get selected SharePoint sites:
[array]$sites = @()
if ($spSite) {                 #If spSite is specified, search only one site:
    $sites += Get-SPSite -Identity $spSite -Limit All
} elseif ($webApplication){    #If webApplication is specified, search all sites in the webapp:
    $sites += Get-SPSite -WebApplication $webApplication -Limit All
} else {                       #Otherwise, search all web applications defined on the local farm:
    $sites += Get-SPSite -Limit All
}

##Begin Main Loop:
foreach ($site in $sites) {
    $webs = @()
    # Gets webs in current site
    $webs = $site | % {Get-SPWeb -Site $_ -Limit All}
    foreach ($web in $webs) {
        Write-Host "Testing web:" $web.url -ForegroundColor cyan 
        $webPerms = @()
        $webPerms = $web.Permissions
        foreach ($perm in $webPerms) {
            #Scenario 1: ACL entry is for this specific user:
            if ($perm.member.loginName -match $regexDomainUsers) {
                [string]$user = ($perm.member.loginName).split('\') | select -Last 1
                write-host "    Found match $user in the web ACL list." -ForegroundColor yellow
                [string]$aclData = 'Acl:Direct'
                [string]$out = $user + ',' + (getPermType($perm.PermissionMask)) + ',' + $web.Url + ',' + $aclData
                $out | Out-File -Append -FilePath $logPath
            }
            #Scenario 2: ACL entry is an Active Directory Group
            if ($perm.Member.IsDomainGroup) {
                [String[]]$ADGroupusers = @()
                $ADGroupUsers = checkADGroupMembers -adGroupName $perm.Member.LoginName -adGroupSid $perm.Member.Sid -userRegex $regexUsers
                if ($ADGroupUsers.count -gt 0) {
                    foreach ($user in $ADGroupUsers) {
                        write-host "    Found user $user in AD Group:" $perm.member.loginName ", which is on the web ACL list." -ForegroundColor yellow
                        [string]$aclData = 'Acl:Embedded:' + $perm.Member.LoginName 
                        [string]$out = $user + ',' + (getPermType($perm.PermissionMask)) + ',' + $web.Url + ',' + $aclData 
                        $out | Out-File -Append -FilePath $logPath
                    }
                }
            }
            #Scenarios: ACE is for a SharePoint group:
            if ($perm.Member.GetType().Name -eq 'SPGroup') {
                $members = @()
                $members += $perm.member.users
                foreach ($member in $members) {
                    #Scenario 3: ACL is a SharePoint group, and the group contains a matching user:
                    if ($member.loginName -match $regexDomainUsers) {
                        [string]$user = ($member.loginName).split('\') | select -Last 1
                        write-host "    Found match for" $user "in a Sharepoint group that is in the web site ACL." -ForegroundColor yellow

                        [string]$aclData = 'SPGroup:Direct:' + $perm.Member.LoginName
                        [string]$out = $user + ',' + (getPermType($perm.PermissionMask)) + ',' + $web.Url + ',' + $aclData
                        $out | Out-File -Append -FilePath $logPath
                    } #End Scenario 3
                    #Scenario 4: ACL is a SharePoint group, and the group contains an AD group that contains a matching user:
                    if ($Member.IsDomainGroup) {
                        [String[]]$ADGroupUsers = @()
                        $ADGroupUsers = checkADGroupMembers -adGroupName $Member.LoginName -adGroupSod $Member.LoginName.Sid -userRegex $regexUsers
                        if ($ADGroupUsers.count -gt 0) {
                            foreach ($user in $ADGroupUsers) {
                                write-host "    Found user $user in an AD Group that is found in a SharePoint group that appears in the web site ACL." -ForegroundColor yellow
                                [string]$aclData = 'SPGroup:Embedded:' + $perm.Member.LoginName + ':' + $member.loginName
                                [string]$out = $user + ',' + (getPermType($perm.PermissionMask)) + ',' + $web.Url + ',' + $aclData
                                $out | Out-File -Append -FilePath $logPath
                            }
                        }
                    } #End Scenario 4
                }
            } #End SPGroup Eval
        } #End foreach $perm
        foreach ($admin in $web.SiteAdministrators) {
            #Enumerate Web Site Administrators (should be at least two)
            if ($admin.LoginName -match $regexDomainUsers) {
                [string]$user = $matches.user
                write-host "    Found web site administrator match for:" $user -ForegroundColor yellow
                [string]$out = $user + ',' + 'webSiteAdministrator,' + $web.Url
                $out | Out-File -Append -FilePath $logPath
                #exit #for debugging
            }
        }
        $web.Dispose()
    }
    if ($site.owner.UserLogin -match $regexDomainUsers) {
        #Discover Site Owner (should be only one)
        [string]$user = $matches.user
        write-host "    Found site collection owned by:" $user -ForegroundColor yellow
        [string]$out = $user + ',' + 'SiteCollectionOwner,' + $site.Url
        $out | Out-File -Append -FilePath $logPath
        #exit #for debugging
    }
    $site.Dispose()
}

SharePoint Tip-of-the-day: Speed up Servicing

Hats off to Russ Maxwell over at MSDN blogs for this hot tip on SharePoint servicing:

http://blogs.msdn.com/b/russmax/archive/2013/04/01/why-sharepoint-2013-cumulative-update-takes-5-hours-to-install.aspx

I am getting back (finally) to working on our SharePoint 2013 migration, and being reminded of how much I hate servicing the SharePoint stack.  How can installing a few hundred megabytes of SharePoint bits take so much time?!?!?

It turns out you can speed up installation of service packs and cumulative updates simply by stopping (or pausing) Search services, the SharePoint Timer service, and the IIS Admin service.  I tried it, and it works!  Installation of the SharePoint 2013 September 2014 CU took not more than a minute with these services halted.  (At least, it did on three out of four servers… the fourth failed miserably owing to MSI errors.

More on that…

I found the following old, but still relevant, article on troubleshooting Office software installation problems:

http://support2.microsoft.com/kb/954713/en-us

To summarize, you go to your %temp% directory and look for “Opatchinstall(#).log” and “######_MSPLOG.log.” (In my case, there was a file called something like “wss##_MSPLOG.log.  Old-school SharePoint guys will recognize “WSS” as “Windows SharePoint Services”).  Try to locate a line containing “MainEngineThread is returning”, and look up the error code that was returned here:

Mine was error code 1646, or “ERROR_PATCH_REMOVAL_UNSUPPORTED: The patch package is not a removable patch package. Available beginning with Windows Installer version 3.0.”  Apparently the language pack for SP2013 Standard that I was using refused to uninstall.  That’s the legacy of excessive servicing from early release versions, I guess.  Since I was still running on Server 2012 (R1), I decided to nuke and repave rather than troubleshoot.  Boo.

Coping with Renamed user Accounts in sharepoint

Yesterday I received a strange error report from a person trying to create a new SharePoint site collection.  Our front line guy went to investigate and found that she was getting a “User cannot be found” error out of SharePoint when attempting to complete the self-service site creation process.  This person reported that her last name changed recently, along with her user ID, yet SharePoint will still showing her as logged in under her old name.

Linking the “Correlation ID” up to the diagnostic logs was of no great help.  The diagnostic logs simply reported “User cannot be found” when executing the method “Microsoft.SharePoint.SPSite.SelfServiceCreateSite”.  We are able to see that “ownerLogin”, “ownerEmail”, and “ownerName” strings were being passed to this function, but not what the values of those strings were.  I guessed that the web form was passing the person’s old account login name to the function, and that since this data was no longer valid, an error was getting displayed.  But how to fix this?

SharePoint 2010 (and WSS 3.0 before it) keeps a list of Site Users that can be accessed using the SharePoint Web “SiteUsers” property. This list is updated every time a new user logs in to the site.  The list entries contain username, login identity, email address, and security ID (SID) data.  It also appears that Site User data is not updated when user data changes in Active Directory (as long as the SID stays the same, that is).  Additional user account data is stored in XML data in the SharePoint databases, and can be accessed using the SharePoint Web “SiteUserInfoList” property.  All of this data needs to be purged from the root web site so that our hapless user can once again pass valid data to the SelfServiceCreateSite method.

Presumably the Site Management tools could be forced to get the job done, but the default views under SharePoint 2010 are hiding all site users from me, even when I log in as a site administrator.  Let’s try PowerShell instead:

add-pssnapin microsoft.sharepoint.powershell 
$root = get-spweb -identity "https://sharepoint.uvm.edu/" 

# "Old ID" below should be all or part of the user's original login name: 
$oldAcc = $root.SiteUsers | ? {$_.userLogin -match "oldID"} 
#Let's see if we found something: 
$oldAcc.LoginName 

#Remove the user from the web's SiteUsers list: 
$root.SiteUsers.Remove($oldAcc.LoginName) 
$root.Update() 
#Let's see if it worked: 
$id = $oldAcc.ID 
$root = get-spweb -identity "https://sharepoint.uvm.edu/" 
$root.SiteUsers.GetByID($id) 
# (This should return a "User cannot be found" error.) 

#Now to see what is in SiteUserInfoList: 
$root.SiteUserInfoList.GetItemById($id) 
# (This data can be cleaned up in the browser by visiting:
# " /_catalogs/users/simple.aspx" 
# from your site collection page.)

SharePoint 2010 – Email Alerts to Site Administrators

We are in the final stages of preparation for the long-overdue upgrade to SharePoint 2010.  I have set up a preview site with a copy of the production SharePoint content database, and I want to notify all site owners that they should check out their sites for major problems.  How to do?  PowerShell?  Absolutely!


Set-PSDebug -Strict
Add-PSSnapin -Name microsoft.SharePoint.PowerShell

[string] $waUrl = "https://sharepoint2010.uvm.edu"
[string] $SmtpServer = "smtp.uvm.edu"
[string] $From = "saa-ad@uvm.edu"

$allAdmins = @()

[string] $subjTemplate = 'Pending Upgrade for your site -siteURL-'
[string] $bodyTemplate = @"
Message Body Goes Here.
Use the string -siteURL- in the body where you want the user's site address to appear.
"@

$wa = Get-SPWebApplication -Identity $waUrl

foreach ($site in $wa.sites) {
	#Write-Host "Working with site: " + $site.url
	$siteAdmins = @()
	$siteAdmins = $site.RootWeb.SiteAdministrators
	ForEach ($admin in $siteAdmins) {
		#Write-Host "Adding Admin: " + $admin.UserLogin
		[string]$a = $($admin.UserLogin).Replace("CAMPUS\","")
		[string]$a = $a.replace(".adm","")
		[string]$a = $a.replace("-admin","")
		[string]$a = $a.replace("admin-","")
		if ($a -notmatch "sa_|\\system") { $allAdmins += , @($a; [string]$site.Url) }
	}
	$site.Dispose()
}

$allAdmins = $allAdmins | Sort-Object -Unique
#$allAdmins = $allAdmins | ? {$_[0] -match "jgm"} | Select-Object -Last 4

foreach ($admin in $allAdmins) {
	[string] $to = $admin[0] + "@uvm.edu"
	[string] $siteUrl = $admin[1]
	[string] $subj = $subjTemplate.Replace("-siteURL-",$siteUrl)
	[string] $body = $bodyTemplate.Replace("-siteURL-",$siteUrl)
	Send-MailMessage -To $to -From $From -SmtpServer $SmtpServer -Subject $subj -BodyAsHtml $body
}

SharePoint 2010 – Authentication and Browser Support Planning

We have gone back to the drawing board with SharePoint 2010 planning, and now are challenging some ideas about how authentication must be configured for SharePoint to work with our clients.  Previously, we felt the need to provide multiple supported authenticaiton types (Windows, Basic, and Forms) hosted on different IIS web sites, with unique URL’s (sharepoint, sharepointlite, partnerpoint).  We also felt the need to have a version of sharepoint with “Client Integration” features enabled, and one with these features disabled (sharepointlite).  With changes in SharePoint 2010, is this really necessary?

First, let’s look at the Windows vs. Basic assumption.  Are there any browsers out there that do not support Windows/NTLM authentication?  In fact, there are… the Android mobile browser and, um…, well, there do not appear to be any others.  However, it is not necessary to make a separate web site available to allow basic authentication.  If we enable basic auth on an exsiting Windows-auth web site, browsers that do not support Windows auth suddenly start working. (Update, 2013-02-36:  Chrome for Android now supports NTLM authentication, so there appears to be no need to support Basic authentication at all at this point in time.)

Now, let’s look at the client integrated vs. dumbed-down assumption.  Previously, we wanted to ensure the clients handled links to MS Office documents stored in SharePoint in a predictable fashion.  Users of Firefox and Safari frequently complained about “strange” document handling behavior in SharePoint links.  For people experiencing confusion caused by failed attempts to launch Office applications from SharePoint browser links, we exposed a version of SharePoint that had the client integration features disabled.  HOWEVER, under SharePoint 2010, the client integration features are much more reliable.  On Windows, I am able to make use of Office 2010 client integration in both IE9 and Firefox.  On the Mac, client integration works with Office 2011 Mac and Safari 5 or FireFox 8.  Additionally, SharePoint will detect if a browser cannot support client integration, and will disable Office inegration links automatically.  For example, the “Open in Word” link is greyed-out in my Chrome browser, while the “Download file” link is active.

Add to this the new mobile web version of SharePoint 2010.  If you connect from a browser listed in the “compat.browser” file on the SharePoint web server, you get directed to a light-weight mobile version of SharePoint instead.  See: http://blogs.technet.com/b/office2010/archive/2010/03/09/configure-sharepoint-server-2010-for-mobile-device-access.aspx for more details on how “compat.browser” works.  This version will use Office Web Apps to render Office documents, but editing will not be possible.  This grants mobile users a functional (if somewhat limited) access method for SharePoint content, while at the same time sidestepping the issue of client integration.  It also means that we do not have to deal with the hassle of attempting to ensure that Office Web Apps will work in a myriad of underpowered mobile browsers.

All things considered, things are looking bright for SharePoint 2010.  It seems we no longer will need a “lite” version of SharePoint, and we will not need two URLs to support the varying authentication needs of legacy browsers.

NTLMv2 – Troubleshooting Notes – NTLM Negotiation is a lie!

We are now in the process, several years late, of trying to disallow use LM and NTLM authentication on our campus. First attempt? Train Wreck!

For stage 1 of enforcement we chose to restrict our domain controllers to accept only NTLMv2 authentication, and reject LM/NTLM. Our thinking was that clients are supposed to negotiage for the the best auth security available, so by forcing NTLMv2 at the DC, we would be able to quickly flush out non-compliant clients. Futher, we assumed that there would not be a lot of non-compliant clients. Guess what? We were WRONG WRONG WRONG

You know which clients are non-compliant? Windows 7, IE8, IE8, FireFox 4b10, Chrome 9. What? Really? It’s true… when accessing IIS web servers configured to use Windows authentication, with NTLMv2 enforced on the authenticating server, even Windows 7 will negotiate down to the weakest possible NTLM level.   So… access to SharePoint 2007 from IE 9 beta on Win 7 falls back to welcome-to-the-80s LM authentication.  This is very shocking, and annoying. When inspecting authentication negotiation traffic to the web server, we see that both client and server have indicated that NTLMv2 is supported, and that LM should not be used. However, when the client sends its authentication challenge response, it sends NTLM and LM responses only and not NTLMv2. Only by setting local security policy on the Windows client to “NTLMv2 only” are we able to prevent the sending of LM/NTLM. Negotiation is a lie!

So, we are going to take this us with MS Support. Some resources that may be helpful going forward:

http://technet.microsoft.com/en-us/magazine/2006.08.securitywatch.aspx
A good explanation of how the “LMCompatibilityLevel” security setting works.  Contains one major factual error… it claims that clients and servers will always negotiate the highest mutually-support LM auth level.  This may be true for SMB/CIFS.  It certainly is not true for web auth.

http://kb.iu.edu/data/bamu.html
Over at Indiana University, LM/NTLM has been disabled for some time.  They have thorough documentation on configuring workstations to function properly in their more restrictive environment.  This page concerns the use of SharePoint at IU, and the required configuration settings for various browsers and operating systems for their network.  There is nothing specific to our problem here, but there is the implication that they experienced problems similar to our own.  All of their docs state that you must configure your client to perform NTLMv2 only, or authentication will fail.  Also included are notes on FireFox and Mac platforms, where client-side configuration apparently is required.
http://kb.iu.edu/data/atpq.html
An additional page that suggests that network clients will not be able to authenticate to IU resources unless they disabled NTLMv2 on the client.

Concerning FireFox on Windows:
http://forums.mozillazine.org/viewtopic.php?f=23&t=1648755
A developer notes that as of FF 3.6, FF uses Microsoft APIs for NTLM authentication.  So, if FF is not performing NTLM auth as you expect, you need to look to your OS for the source of the problem.  It’s true… we confirmed via packet capture that FF 4.0b10 and IE9RC both perform NTLM authentication in exactly the same way.

Concerning Macintosh client settings:
http://discussions.apple.com/thread.jspa?threadID=2369451
Recommended smb.conf file settings to enforce use of NTLMv2 on the Mac.  References are made here to an “nsmb.conf” file, which is not a typographic error.  More on this file here:
http://developer.apple.com/library/mac/#documentation/Darwin/Reference/ManPages/man5/nsmb.conf.5.html

Office Web Apps and SharePoint 2010

Office web apps are here!

Where do I get them? From Microsoft Volume Licensing of course:
http://licensing.microsoft.com
You need to look in an unexpected place, though. Under the “Office” section, select “Office Professional Plus 2010″, then select the 32-bit edition (not 64-bit… weird!). Take note of the Web Apps product key in the product selection page.

Installation instructions can be found here:
http://technet.microsoft.com/en-us/library/ff431687.aspx#
And they are pretty good, with the exception of one PowerShell syntax error…

When you get to the stage of installing Service Application Proxies, if you choose to use the PowerShell method, you are given a small script that starts with the following command:

$appPool = Get-SPServiceApplicationPool -Name "SharePoint Web Services Default"

This command will not work as posted. “-Name” should be “-Identity”. A more reliable command might read:

$appPool = Get-SPServiceApplicationPool | where-object {$_.Name -match "SharePoint Web Services Default"}

Preparing the SharePoint 2007/WSS3 Database for Upgrade

  1. Run pre-upgrade check:
    stsadm –o preupgradecheck
    (to be run on the WSS 3 server… requires WSS 3.0 SP2 and October 2008 CU).
    Now, repair any problems that were reported…
  2. Delete orphaned sites:
    1. run: stsadm -o enumallwebs
      – find sites with “InSiteMap” set to “false”.
    2. take note of the SiteID GUID, then run:
      stsadm -o deletesite -force -siteid [siteid] -databaseserver [dbserver] -databasename [dbname]
    3. Delete references to missing web parts:
  3. The upgrade check report tells us:
    “ The following web part(s) are referenced by the content, but they are not installed on the web server Id = dcdbbbd0-8dd6-1ecb-a3b2-12d30061d482, Type = Unknown, Reference = 7, Status = Missing …”
    but there is no particular advice on how to fix the problem. I wasted a few hours trying to enumerate web parts in use by a site using PowerShell, and ended up digging around in the Content database using TransactSQL… here is what works:

    1. Run the following against the content database:
      use [contentDatabaseName]
      select DirName,LeafName from dbo.AllDocs where id in
      (select tp_PageUrlID from dbo.WebParts where
      (tp_WebPartTypeID='dcdbbbd0-8dd6-1ecb-a3b2-12d30061d482')
      OR (tp_WebPartTypeID='d5101cfe-e315-c578-cd06-1966f283e3ed')
      OR (tp_WebPartTypeID='602e7431-ac3e-75b9-c8e0-57533bdab161'))
    2. Access the page returned by the above query, appending “?Contents=1″.  Delete any web parts reporting errors.
  4. We are informed that various features are referenced by content, but that the features are not available on the server.  We are given only feature IDs.  Back to SQL:
    • use STSContentDBPrime
      select FullUrl from [Webs] where Id in (
      select WebId from [Features] where (FeatureId = 'bbe9def7-2fe9-a0b1-d712-aa128c837ebe')
      OR (FeatureId='bbe9def7-2fe9-a0b1-d7bb-aa128c837ebe')
      )
    • This returns the sites that are using these “bad features”.  But what to do about it?
    • http://www.gilham.org/Blog/Lists/Posts/Post.aspx?ID=229 suggests the use of “WSSAnalyzeFeatures” and “WSSRemoveFeatureFromSite“, both of which work to purge the evil old CKS:Enhanced Blog edition feature from the one site that was using it… sheesh!
  5. We are warned that “The following site definition(s) are referenced by the content, but they are not installed on the web server”. The name of the missing site definition is “Unknown”. We are given only a “template id” (11003) and a Language Code (1033). Finding where this definition is being used can be accomplished with a very small sql query:
    use SharePoint_Content_1 
    SELECT Title, MasterURL, WebTemplate FROM dbo.Webs where WebTemplate='11003'
    
  6. We are warned that “setup files” are referenced in the database that are not present on the server. We are given the names and paths of the files that are missing, but not the web sites that reference them. The offending site can be tracked down quickly using “stsadm”. Run this command:
    stsadm -o enumAllWebs -includesetupfiles > allWebs.txt
    Then search the resultant file for one or more of the filenames listed in the pre-upgrade check report. In this case, I was able to locate a site that used “SharePoint Learning Kit” components. This site was completely broken, since we removed the Learning Kit over a year ago. I got approval from the site owner, and deleted the offending site.
  7. We are warned that the feature “f374a3ca-f4a7-11db-827c-8dd056d89593″ is referenced by over 500 sites, but that the feature is no longer present on the server.  This feature is the “RadEditor for MOSS for IE”, which in fact has been phased out.  Lets use a handy “for” loop.
    1. First we export the SQL query we used above as a CSV file (this time searching for the “f374a…” feature instead).
    2. We use that csv as fuel for our “for” loop:
      for /f %i in (H:\BadFeatureSites_2010.csv) do WssRemoveFeatureFromSite.exe -scope site -url https://sharepoint.uvm.edu/%i -featureid f374a3ca-f4a7-11db-827c-8dd056d89593 -force
    3. Oh but look… WssRemoveFeatureFromSite can’t deal with sites that have spaces in the URL (why would you have a space in your site name, right?).  dang!  Let’s try “SharePoint Feature Administration and Clean Up Tool“… it works!
  8. We are told that we are missing some additional template files such as:
    Path = [C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\Template\1033\MPS\..\sts\lists\wplib\dwp\MSContentEditor.dwp], Reference = [2], Status = [Missing]

    • Some searching discovered KB839877, which informs us that an earlier version of the “Workspace Meeting” template contained an accidental double period when a single period was intended.  But how do we find the site that is using this bad template?
    • The excellent tool “SharePoint Manager” from Carsten Keutman allows us to explore the SharePoint object model to more quickly find Site Template data that we need in a site.
    • The really really excellent tool “PowerGUI Script Editor” was used to quickly develop this script:
      # Discovers Meeting Workspaces from all "Webs" in the SharePoint web application defined in the "webAppUrl" variable.
      # Outputs discovered data to file "sitelist.csv
      # Greg Mackinnon, 2010-02-05
      [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint") | Where-Object { $_.GetType().FullName -ne "System.Reflection.Assembly" }
      $webAppUrl = "https://sharepoint.uvm.edu"
      $wa = [Microsoft.Sharepoint.Administration.SPWebApplication]::Lookup($webAppURL)
      
      #[string]$out = "List of sites using the Meeting Workspace template"
      Remove-Item -Path .\sitelist.csv -Force
      
      foreach ($site in $wa.sites) {
      foreach ($web in $site.AllWebs) {
      Write-Output "Checking web: " + $web.Url
      if ($web.WebTemplate -match "MPS") {
      Out-File .\sitelist.csv -InputObject $web.ID.Guid -NoClobber -Append -Encoding Unicode
      }
      }
      $web.Dispose()
      }
      $site.Dispose()
      #Out-File .\sitelist.csv $out -Force
    • The script returns a list of “web” object GUIDs.  We feed this into SQL to discover where in the content database the sites that are using these web parts are implemented:
      use STSContentDBPrime
      --select DirName,LeafName,SetupPath from dbo.AllDocs where WebId='abee2623-56d2-43d6-8a9c-7b362fbd323b'
      select DirName,LeafName,SetupPath
      from dbo.AllDocs
      where ((WebId in ('siteGuid1','siteGuid2','...')) AND (SetupPath LIKE '%MSContentEditor.dwp%'))
    • Finally, the SQL query finds the “Meeting Workspace” sites that are using this corrupted web part.  We probably could just delete the web parts in question from the web part gallery, but I chose instead to use the stsadm.exe “export”, “deletesite”, “createsite”, and “import” commands to re-create the sites.  The export utility filtered out the corrupt content for us.  Once restored, the discovered sites no longer contained corrupted web parts.   Phew!
  9. The WSS3 instance has been prepped… Clone existing content database, and proceed with upgrade testing!

ADFS 2 and SharePoint 2010 Integration

Here is a quick entry on ADFS2 and SharePoint 2010 integration. It is not an implementation guide or end-to-end walkthough… that comes later, if we decide to implement this thing.

At present, I am most interested in the model of SharePoint->ADFS2->Shibboleth, where the SP-STS trusts tokens from ADFS2.  ADFS2 is part of a chained federation with our Shib service.  ADFS will consume Shib tokens, then transform them for the benefit of SharePoint.  However, I have no idea how to implement this solution at this time.

There are a few-too-many blog entires out there detailing how to configure ADFS2 and SharePoint 2010 for integration.  Trouble is, many of the step-by-step guides present contradictory configuration steps.  I guess there is no substitute for a deep, working knowledge of ADFS 2, SAML, and other Federation topics.

Here are some of the claims setup guides I have been working with:

Here are additional configuration posts on the process of upgrading an existing SharePoint Web Application from “Windows” authentication to “Claims” authentication.  The common denominators?

  1. You must add a valid new user to your claims-aware web app before migrating existing users, or the web application will be inaccessible after migration (or indeed, even before migration!)
  2. To trigger migration of users, you must invoke the “Migrate Users” method on your web app, E.g.:
    $wa = get-SpWebApplication "https://webappurl"
    $wa.MigrateUsers($true)

The things here that seem very unclear to me are:  What exactly is being done when you invoke the “MigrateUsers” method on the Web Application object?  How does SharePoint map legacy “Windows” users to new “Claims” users?  Anyway, here are the links:

Pages containing information that I have found useful while contemplating how to pull off SharePoint 2010:

Many of these links, as it turns out, were already discovered by members at dirteam.com.  Doh…

http://blogs.dirteam.com/blogs/jorge/archive/2010/07/06/configuring-sharepoint-2010-to-use-adfs-v2-as-an-authentication-provider.aspx

ADFS 2 Setup and Configuration

The following is a work in progress… Everything done so far works, but this is not yet a complete end-to-end deployment guide…

ADFS 2: new… improved?

There were lots of setup and configuration guides for ADFS 1.0, even though the product was nauseatingly difficult to understand.  Along comes ADFS 2… new, more powerful, better, more standards compliant… documentation reads like scrawl on a napkin.

Some setup quirks:

  • Don’t forget that no current version of Windows Server includes ADFS 2.0… not even Server 2008 R2.  If you want to install ADFS 2.0 you must download it from MS and install.  DO NOT add the out-of-box ADFS role on Server 2008 R2.  It will confuse you, because it does not disclose which version of ADFS that it is.
  • Since we are planning a farm, generate a farm SSL certificate on one of the ADFS servers, then export the certificate in PFX format (that is, with the private key), and restore the cert to the second ADFS server.
  • It is considered desirable to put the ADFS database on a reliable external database server, but documentation on this options is limited to command line help.  Here is what I did:
    1. On one of the ADFS servers, run:
      Fsconfig.exe GenerateSQLScripts /ServiceAccount [account] /ScriptDestinationFolder [destination folder]
    2. Install the appropriate version of the SQL Native Client on the ADFS servers.
    3. On the SQL server, run the two scripts that were generated by the above command.  This will create two databases… AdfsConfiguration and AdfsArtifactStore, and set permissions for the service account specified.
    4. Make sure that any firewalls on the SQL server will allow the ADFS servers to connect to it.
    5. Since we use SQL mirroring, set up mirrors for these databases.  Add the service account to the list of accepted logins on the mirror server, since this will not be done automatically.
    6. Generate certificates that will be used for the ADFS web site, token signing, and token decryption.  Put the certificates in the Windows certificate store of the local computer.
    7. Preconfigure binding of the SSL certificate to the default IIS web site.
    8. Configure the first server in the farm using the following syntax:
      Fsconfig.exe CreateSQLFarm /ServiceAccount [Service Account]
      /ServiceAccountPassword [password] /SQLConnectionString "Initial Catalog=AdfsConfiguration; Data Source=spellbound; failover partner=rearwindow; integrated security=SSPI; Network Library=DBMSSOCN" /FederationServiceName login2.uvm.edu /CleanConfig /CertThumbprint "[thumbprint]" /SigningCertThumbprint "[thumbprint]" /DecryptCertThumbprint "[thumbprint]"
      Note that the thumbprint can be obtained by viewing the properties of the certificate in Windows explorer.
    9. Note that the ADFS “Artifact Store” database should get configured automatically, but you can ceck on this by doing the following:
      1. Launch PowerShell with admin privs
      2. Run the command “Add-PSSnapin microsoft.adfs.powershell”
      3. Run “get-adfsproperties | select artifactdbconnection”
      4. Use “set-adfsproperties -artifactdbconnection” to change the string if necessary.
      5. See this resource for more details:
        http://social.technet.microsoft.com/wiki/contents/articles/ad-fs-2-0-migrate-your-ad-fs-configuration-database-to-sql-server.aspx

Of course, an ADFS server with no Federation partners does not do us much good.  Unfortunately, I don’t have any other ADFS servers that I want to integrate with, either.  What interests me more are Shibboleth services (such as our own “login.uvm.edu”), and federation with other “InCommon” partners.  I also am interested in “user centric” identity providers such as OpenID and “Windows Live ID”.  Here are some links to get us started down this path.  Unfortunately, I cannot find anything that details exactly what we want: