Category Archives: Office 365

Azure AD Integration

This year I’ve been working a lot more with Azure. On of my tasks has been to integrate other application to each other using Azure AD. Here are some of my finding and good to know things in case someone else runs into them:

  • Create a new MVC Application in Visual Studio
  • When this is done, press the second mouse button on the project and go to “Add” > “Connected Services”. This will allow you to create an O365 connection.
  • Select Office 365 API Services.
  • In the new Window select or type in your domain.
  • Next, create new Azure AD application configuration or use an existing one by providing the GUID for the application.
    • Make sure you select the “Configure Single Sign-On using Azure AD” option
    • Make sure that your application is multi-tenant:
      • This works so that you register your app with you own Azure AD domain, then after that external Azure AD tenants and their users are registered through an “onboarding” process. The process will the user or admin user for privileges to use certain information from the AD or other resources. These are defined in the Azure AD application settings.
      • Notice you are using an application ID and key to connect to your own organization Azure AD then the users are only onboarding using the multi-tenant option in the Azure AD application configuration.
    • Next select what kind of privileges your application needs from the Azure AD and O365.
    • You need onboarding functionality from here: https://azure.microsoft.com/en-us/documentation/samples/active-directory-dotnet-webapp-multitenant-openidconnect/
    • In Global.asax.cs application_start function add the following: AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier;
      • If this is missing, then you claim will not work properly.
    • If you are using a SQL Server database and Entity Framework remember to update you model from the database and remember primary key connections. If the Entity Framework update does not work then removing and adding the database tables should force an update. Also remember to clean and build your project if nothing else helps.
    • If you get this error: Error: {“error”:”invalid_grant”,”error_description”:”AADSTS70002: Error validating credentials. AADSTS70000: The provided access grant is invalid or malformed…..
    • Doing a redirect the proper way in an MVC apllication using the following piece of code in your contoller: return Redirect(returnUrl);
      • If you use the normal way in ASP .NET: Response.Redirect(returnUrl); you will run into trouble. The error message might look something like this:
        • Server Cannot Append Header After HTTP headers have been sent Exception at @Html.AntiForgery
        • You could set the AntiForgeryConfig.SuppressXFrameOptionsHeader = true; in the Application_start, but this will lower your security and not advisable.

 

SharePoint number fields and search

If you are having problems making searches with number fields then you have to add them to a managed property in the Search Schema in your Search Service. Make sure that you tap the check boxes that the property is searchable. Also, make sure that the managed property data type is set to text.

SharePoint change document set and items content type to a new content type

I’ll put it simply:

This is a PowerShell script that you can use to change a content type of a library or list to another one. This script can identify between Document Sets and library or list items.


param (
[string]$WebsiteUrl = "http://portal.spdev.com/",
[string]$OldCTName = "DSTestCT",
[string]$NewCTName = "DSTestCT"
)

if ( (Get-PSSnapin -Name MicroSoft.SharePoint.PowerShell -ErrorAction SilentlyContinue) -eq $null )
{
Add-PsSnapin MicroSoft.SharePoint.PowerShell
}

function Reset-ListContentType ($WebUrl, $ListName, $OldCTName, $NewCTName)
{
$web = $null
try
{
$web = Get-SPWeb $WebUrl

$list = $web.Lists.TryGetList($ListName)
$oldCT = $list.ContentTypes[$OldCTName]

$isChildOfCT = $list.ContentTypes.BestMatch($rootNewCT.ID).IsChildOf($rootNewCT.ID);
if($oldCT -ne $null -and $isChildOfCT -eq $false)
{
$hasOldCT = $true
$isFoldersCTReseted = Reset-SPFolderContentType –web $web -list $list –OldCTName $OldCTName –NewCTName $NewCTName
Reset-SPFileContentType –web $web -list $list –OldCTName $OldCTName –NewCTName $NewCTName
Remove-ListContentType –web $web -list $list –OldCTName $OldCTName –NewCTName $NewCTName
if($hasOldCT -eq $true)
{
Add-ListContentType –web $web -list $list –OldCTName $OldCTName –NewCTName $NewCTName
if($isFoldersCTReseted -eq $true)
{
Set-SPFolderContentType –web $web -list $list –OldCTName $OldCTName –NewCTName $NewCTName
}
}
}


}catch
{

}
finally
{
if($web)
{
$web.Dispose()
}
}

}

function Remove-ListContentType ($web, $list, $OldCTName, $NewCTName)
{


$oldCT = $list.ContentTypes[$OldCTName]

$isChildOfCT = $list.ContentTypes.BestMatch($oldCT.ID).IsChildOf($oldCT.ID);

if($isChildOfCT -eq $true)
{
$list.ContentTypes.Delete($oldCT.ID)
}
$web.Dispose()

return $isChildOfCT
}

function Add-ListContentType ($web, $list, $OldCTName, $NewCTName)
{



$list.ContentTypes.Add($rootNewCT)

$web.Dispose()
}

function Reset-SPFolderContentType ($web, $list, $OldCTName, $NewCTName)
{
#Get web, list and content type objects

$isFoldersCTReseted = $false


$isChildOfCT = $list.ContentTypes.BestMatch($rootNewCT.ID).IsChildOf($rootNewCT.ID);

$oldCT = $list.ContentTypes[$OldCTName]
$folderCT = $list.ContentTypes["Folder"]
$newCT = $rootNewCT

$newCTID = $newCT.ID

#Check if the values specified for the content types actually exist on the list
if (($oldCT -ne $null) -and ($newCT -ne $null))
{
$list.Folders | ForEach-Object {

if ($_.ContentType.ID.IsChildOf($rootNewCT.ID) -eq $false -and $_.ContentType.ID.IsChildOf($oldCT.ID) -eq $true -and $_.Folder.ProgID -eq "Sharepoint.DocumentSet")
{
Write-Host "Found a document set: " $_.Name "Processing document set"
$item = $list.GetItemById($_.ID);
$item["ContentTypeId"] = $folderCT.Id
$item.Update()
$isFoldersCTReseted = $true
}
}
}

$web.Dispose()

return $isFoldersCTReseted
}

function Set-SPFolderContentType ($web, $list, $OldCTName, $NewCTName)
{
#Get web, list and content type objects



$folderCT = $list.ContentTypes["Folder"]
$newCT = $list.ContentTypes[$NewCTName]

#Check if the values specified for the content types actually exist on the list
if (($newCT -ne $null))
{
$list.Folders | ForEach-Object {
if ($_.ContentType.ID.IsChildOf($newCT.ID) -eq $false -and $_.ContentType.ID.IsChildOf($folderCT.ID) -eq $true -and $_.Folder.ProgID -eq "Sharepoint.DocumentSet")
{
$item = $list.GetItemById($_.ID);
$item["ContentTypeId"] = $newCT.Id
$item.Update()
}
}
}

$web.Dispose()
}


function Reset-SPFileContentType ($web, $list, $OldCTName, $NewCTName)
{
#Get web, list and content type objects



$isChildOfCT = $list.ContentTypes.BestMatch($rootNewCT.ID).IsChildOf($rootNewCT.ID);

$oldCT = $list.ContentTypes[$OldCTName]
$folderCT = $list.ContentTypes["Folder"]
$newCT = $rootNewCT

$newCTID = $newCT.ID

#Check if the values specified for the content types actually exist on the list
if (($oldCT -ne $null) -and ($newCT -ne $null))
{
$list.Folders | ForEach-Object {
if ($_.ContentType.ID.IsChildOf($rootNewCT.ID) -eq $false -and $_.ContentType.ID.IsChildOf($oldCT.ID) -eq $true)
{
$_["ContentTypeId"] = $folderCT.Id
$_.Update()
}
}
#Go through each item in the list
$list.Items | ForEach-Object {
Write-Host "Item present CT ID :" $_.ContentType.ID
Write-Host "CT ID To change from :" $oldCT.ID
Write-Host "NEW CT ID to change to:" $rootNewCT.ID

#Check if the item content type currently equals the old content type specified
if ($_.ContentType.ID.IsChildOf($rootNewCT.ID) -eq $false -and $_.ContentType.ID.IsChildOf($oldCT.ID) -eq $true)
{
#Check the check out status of the file
if ($_.File.CheckOutType -eq "None")
{
Change the content type association for the item
$item = $list.GetItemById($_.ID);
$item.File.CheckOut()
write-host "Resetting content type for file: " $_.Name "from: " $oldCT.Name "to: " $newCT.Name

$item["ContentTypeId"] = $newCTID
$item.UpdateOverwriteVersion()
Write-Host "Item changed CT ID :" $item.ContentType.ID
$item.File.CheckIn("Content type changed to " + $newCT.Name, 1)
}
else
{
write-host "File" $_.Name "is checked out to" $_.File.CheckedOutByUser.ToString() "and cannot be modified"
}
}
else
{
write-host "File" $_.Name "is associated with the content type" $_.ContentType.Name "and shall not be modified"
}
}
}
else
{
write-host "One of the content types specified has not been attached to the list"$list.Title
return
}

$web.Dispose()
}

$web = Get-SPWeb $WebsiteUrl
$rootWeb = $web.Site.RootWeb;
$rootNewCT = $rootWeb.AvailableContentTypes[$NewCTName]

Foreach ($list in $web.Lists) {
Write-Host $list.BaseType
if($list.Hidden -eq $false -and $list.BaseType -eq "DocumentLibrary")
{
Write-Host "Processing list: " $list.Title
Reset-ListContentType –WebUrl $WebsiteUrl –ListName $list.Title –OldCTName $OldCTName –NewCTName $NewCTName
}
}

$web.Dispose()

 

How to display all of the refinement options in SharePoint 2013 Search results

By default for some reason, I could not show all of the items, I could define how many items are shown and but not get rid of the “show more” links under the refinements.

This JavaScript snippet can help you. It will hide the “short list” containing a minimum of items to be displayed, then it will show the long list and finally it will hide the “show more” link.

</pre>
<script>
 $( document ).ready(function() {
$('#unselLongList').show();
$('#unselShortList').hide();
$('#unselToggle').hide();

});
</script> 

SharePoint Search and changing the Created and LastModified fields

If you want to change the LastModified and Created fields of a list item or a document you can use this code to do that:


SPSecurity.RunWithElevatedPrivileges(delegate()
{
using (SPSite site = new SPSite("site + web url"))
{
using (SPWeb web = site.OpenWeb())
{
web.AllowUnsafeUpdates = true;
SPListItem listItem = web.GetListItem(this.tbItemURL.Text);

if (this.cCreatedDate.SelectedDate != new DateTime())
listItem[SPBuiltInFieldId.Created] = this.cCreatedDate.SelectedDate;
else
listItem[SPBuiltInFieldId.Created] = listItem[SPBuiltInFieldId.Created];

if (this.cModifiedDate.SelectedDate != new DateTime())
listItem[SPBuiltInFieldId.Modified] = this.cModifiedDate.SelectedDate;
else
listItem[SPBuiltInFieldId.Modified] = listItem[SPBuiltInFieldId.Modified];

listItem.UpdateOverwriteVersion();
if (listItem.ParentList.EnableMinorVersions)
{
listItem.File.Publish("SPFileUpload");
}
if (listItem.ModerationInformation != null)
{
listItem.File.Approve("SPFileUpload");
}




web.AllowUnsafeUpdates = false;
}
}
});

The reason why the above is done is because if you simply change the date values and use the UpdateOverwireVersion() it will not be enough. The search will not be able to index right values EVEN if you check the UI or use PowerShell or SharePoint Manager etc. All of those show that the value is OK but actually is not. The manually inserted dates will not show up in search unless the item or document is published and approved. So no matter that SP says remember this :).

another thing is that this code above does not allow to set a LastModified IF you have publishing/versioning enabled. After calling the UpdateOverwriteVersion function the item or document will be checked out and remains in that state until something is done. Once you do that you are back to where you started the modified date is changed automatically by SP.

Do not have a solution yet. Maybe you have?

PowerShell script how to backup lists and restore them in another location through list templates

Ok, this is one way of doing this. Perhaps this is useful in a situation where you need to backup lists with their data from an older SharePoint version and restore it in another SharePoint version. Preferably doing this quickly without worrying about site templates or something more than the list and its data.

The script below gets lists in a target web location, saves them as list templates and download them in the hard drive.

Approach 1: Export the list through Export-SPWeb and restore with Import-SPWeb

param (
 [string]$SPSiteFilter = "http://portal.spdev.com/",
 [string]$SPWebFilter = "http://portal.spdev.com/",
 [string]$SPWebFilterRelative = "",
 [string]$destination = "C:\\temp"
 )
 
 $site = Get-SPWeb $SPSiteFilter
 
 
 # Get the webs lists and save them as list templates
 $lists = $site.lists
Foreach ($list in $lists) {
Write-Host $list.BaseType
 if($list.Hidden -eq $false -and $list.BaseType -eq "GenericList")
 {
 Write-Host $list.Title + " " + $list.RootFolder.ServerRelativeUrl
 
 $filePath = $destination + "\" + $list.Title + ".cmp"
 $itemURL = $SPWebFilterRelative + $list.RootFolder.ServerRelativeUrl
 Write-Host $filePath
 Write-Host $itemURL
 Write-Host $SPWebFilter
 export-spweb -identity $SPWebFilter -path $filePath -itemurl $itemURL –includeusersecurity
 #$list.SaveAsTemplate($list.Title+ " " + $ListTempalteIDPostFix,$list.Title,"Site list Backup",1)
 }
 }

 $site.Dispose()
param (
 [string]$SPSiteFilter = "http://portal.spdev.com/subsite/",
 [string]$source = "C:\\temp"
 )
 
 $site = Get-SPWeb $SPSiteFilter
 
 Get-ChildItem $source -Filter *.cmp | 
Foreach-Object {
 Write-Host $_.FullName
 #Upload the list temaplte to the list templates gallery
 Import-SPWeb -identity $SPSiteFilter -path $_.FullName –force
 
}

$site.Dispose()

Approach 2: As list templates


param (
[string]$SPSiteFilter = &quot;http://site/&quot;,
[string]$SPWebFilter = &quot;http://site/myweb/&quot;,
[string]$ListTempalteIDPostFix = &quot;SU&quot;,
[string]$destination = &quot;C:\\temp&quot;
)

$site = Get-SPSite $SPSiteFilter
$web = Get-SPWeb $SPWebFilter
$templatesLibrary = $site.RootWeb.GetListFromUrl(&quot;/_catalogs/lt/Forms/AllItems.aspx&quot;)

if(!$templatesLibrary)
{
Write-Host &quot;List template gallery not found. Program execution halted.&quot;
return
}

# Get the webs lists and save them as list templates
$lists = $web.lists
Foreach ($list in $lists) {
Write-Host $list.BaseType
if($list.Hidden -eq $false -and $list.BaseType -eq &quot;GenericList&quot;)
{
Write-Host $list.Title
$list.SaveAsTemplate($list.Title+ &quot; &quot; + $ListTempalteIDPostFix,$list.Title,&quot;Site list Backup&quot;,1)
}
}

# Read the list templates from the gallery and save them to the hard drive
$folder = $templatesLibrary.RootFolder
foreach ($file in $folder.Files) {
#Ensure destination directory
$destinationfolder = $destination

#Download file
$binary = $file.OpenBinary()
$stream = New-Object System.IO.FileStream($destinationfolder + &quot;/&quot; + $file.Name), Create
$writer = New-Object System.IO.BinaryWriter($stream)
$writer.write($binary)
$writer.Close()

}
For ($i=$folder.Files.Count; $i -gt 0 -or $i -eq 0; $i--)
{
$folder.Files[$i].Delete()
}

$web.Dispose()
$site.Dispose()

This script uploads the list templates into the targeted SharePoint site.

param (
[string]$SPSite = &quot;http://portal3.dev.com/sites/sc&quot;,
[string]$SiteTemplatesLocation = &quot;C:\\temp&quot;,
[string]$SiteTemplatesGalleryName = &quot;List Template Gallery&quot;
)
# $site = Get-SPSite $SPSiteFilter
# $web = Get-SPWeb $SPSiteFilter
# $templatesLibrary = $site.RootWeb.GetListFromUrl(&quot;/_catalogs/lt/Forms/AllItems.aspx&quot;)
# $Files = $templatesLibrary.RootFolder.Files

Function UploadListTemplate($WebURL, $TemplateFilePath)
{
#Get the Web
$web = Get-SPWeb $WebURL
#Get the List template Gallery Folder
$TemplateFolder = $web.GetFolder($SiteTemplatesGalleryName)
#Get the Files collection
$TemplateFileCollection = $TemplateFolder.Files
#Get the Template file from Local File system
$TemplateFile = Get-ChildItem $TemplateFilePath

#Open the File in Read mode and Add to Templates collection
$TemplateFileCollection.Add(&quot;_catalogs/lt/$($TemplateFile.Name)&quot;, $TemplateFile.OpenRead(), $true)
Write-Host &quot;Done!Template has been uploaded!!&quot;
$web.Dispose()

}

Get-ChildItem $SiteTemplatesLocation -Filter *.stp |
Foreach-Object {
Write-Host $_.FullName
#Upload the list temaplte to the list templates gallery
UploadListTemplate $SPSite $_.FullName

}

This script will create the lists based on the list templates NOTICE that this does not differentiate between existing list templates not uploaded with the script above.

 param (
 [string]$SPSiteFilter = &quot;http://portal3.dev.com/sites/sc&quot;,
 [string]$ListTempalteIDPostFix = &quot;SU&quot;,
 [string]$destination = &quot;C:\\temp&quot;
 )

 $site = Get-SPSite $SPSiteFilter
 $web = Get-SPWeb $SPSiteFilter

 # Create lists based on the list templates
$site.GetCustomListTemplates($web) |
Foreach-Object {
if($web.Lists.TryGetList($_.Name.Replace($ListTempalteIDPostFix,&quot;&quot;)) -eq $null)
{
 $web.Lists.Add($_.Name.Replace($ListTempalteIDPostFix,&quot;&quot;), $_.Description, $_)
 }
}

 $web.Dispose()
 $site.Dispose()
 

If your list has content that is too large to save as a template try to run this script and remember to change to the default value which I think should be: 52428800


$docSize = 500000000
$webservice = [Microsoft.SharePoint.Administration.SPWebService]::ContentService
$webservice.MaxTemplateDocumentSize = $docSize
$webservice.Update()

Prerequisites for custom list templates

SharePoint List must meet the following criteria to move a list by using a list template (source):

  • The list must contain less than 10 megabytes (MB) of list data.
  • You must be a member of a site group with the Manage Lists right on the source site and on the destination site. By default, the Manage Lists right is included in the Web Designer and Administrator site groups.
  • The source site and the destination site must be based on the same site template.

How to use Microsoft Graph to get office groups listed in a native (Console) application

To get groups from using Microsoft Graph you have to do the following things:

  • Register an application using Azure AD and give required privileges to the application
  • Request a token for your application using the client ID and client secret key
  • Make the API call to using REST by specifying that your realm where to get the groups from

Register your application through Azure AD

Go to https://portal.azure.com/ or https://manage.windowsazure.com

Next from the Azure navigation go to your active directory. Next you should see different options in the top area of the your AAD, such as:

Users, Groups, Applications, Domain and so on.

Select the Applications section.

aad1

Next you should get a listing of applications configured in your AAD.

Next from the AAD UI select ADD to add a new application.

aad2

Select what type of an application it is. You can use both option. I used the configurations of an MVC application which allowed me to work just fine. What matters is the client ID, client secret key and the privileges. The redirect URL is not necessary for a Native Application, you can type in anything as long as it is a URL.

aad4aad5

 

After your application is created go to the configure section.

aad6

Here find the client ID and copy store it somewhere to be used later.

Then go to the keys section and add a new key(client secret)

aad7

Then scroll down and go the applications section and add the Microsoft Graph application and select the needed privileges. In this case, you would like at least to be able to read groups.

aad8aad9

That is it, next is some code.

Request a token for your application

This is the piece of code that will get the token from your application. The constants and parameters will be explained soon.

public static string GetTokenForApplication(String realm, String clientId, String clientSecret)
 {
 AuthenticationContext authenticationContext = new AuthenticationContext(Constants.UnifiedApiConnect.AzureADAuthority + realm, false);
 // Config for OAuth client credentials 
 ClientCredential clientCred = new ClientCredential(clientId, clientSecret);

 AuthenticationResult authenticationResult = authenticationContext.AcquireToken(Constants.UnifiedApiConnect.O365UnifiedAPIResource,
 clientCred);
 string token = authenticationResult.AccessToken;
 return token;
 }

Make the API Call to get the groups

This is the code that will get the token and request data from the Graph API


TokenHelper.Token = Program.GetTokenForApplication(realm, clientId, clientSecret);
 List&amp;lt;Group&amp;gt; groups = new List&amp;lt;Group&amp;gt;();
 string APIURL = Constants.UnifiedApiConnect.O365UnifiedAPIResource + "v1.0/" + realm + "/groups?$filter=groupTypes/any(c:c+eq+'Unified')";
 try
 {
 groups = GroupsHttpHelper.GetGroups(APIURL);
 foreach (var group in groups)
 {
 groupsSite.Add(new SiteDirectoryEntity() { Title = group.displayName, URL = String.Format(Program.O365OutlookMailGroupURL, realm, realmLCID, group.mail) });
 }
 }
 catch (Exception ex)
 {
 Logger.Error("Error in processing O365 groups through MS Graph: " + ex.Message + "\n" + ex.StackTrace);
 }

This is the code that will do the request to the Graph API, notice that the data is deserialized into objects.

 


public class GroupsHttpHelper
 {
 public static List&amp;lt;Group&amp;gt; GetGroups(string apiUrl)
 {
 if (String.IsNullOrEmpty(apiUrl)) { throw new ArgumentNullException("apiUrl"); }

 List&amp;lt;Group&amp;gt; groups = new List&amp;lt;Group&amp;gt;();

 string responseContent = GroupsHttpHelper.GetHttpResource(apiUrl);
 var responseObject = JsonConvert.DeserializeObject&amp;lt;GraphResponse&amp;lt;Group&amp;gt;&amp;gt;(responseContent);
 foreach (var item in responseObject.value)
 {
 groups.Add(item);
 }

 return groups;

 }

 public static string GetHttpResource(string url)
 {
 string responseContent = String.Empty;

 string token = TokenHelper.Token;

 var request = (HttpWebRequest)HttpWebRequest.Create(url);

 request.Method = "GET";
 request.Accept = "application/json";
 request.Headers.Add("Authorization", "Bearer " + token);

 var response = request.GetResponse();
 using (var reader = new StreamReader(response.GetResponseStream()))
 {
 responseContent = reader.ReadToEnd();
 }

 return responseContent;
 }
 }

public class Group
 {
 public string accessType { get; set; }

 public bool? allowExternalSenders { get; set; }

 public bool? autoSubscribeNewMembers { get; set; }
 public string description { get; set; }
 public string displayName { get; set; }
 public string[] groupTypes { get; set; }
 public string id { get; set; } // identifier

 public bool isSubscribedByMail { get; set; }
 public string mail { get; set; }

 public bool? mailEnabled { get; set; }
 public string mailNickname { get; set; }
 public string onPremisesLastSyncDateTime { get; set; } //timestamp
 public string onPremisesSecurityIdentifier { get; set; }

 public bool? onPremisesSyncEnabled { get; set; }
 public string[] proxyAddresses { get; set; }

 public bool? securityEnabled { get; set; }
 public int unseenCount { get; set; }
 public string visibility { get; set; }
 }

Namespaces needed in the code:

using Microsoft.Online.SharePoint.TenantAdministration;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using Newtonsoft.Json;

NuGet Packages needed:

Active Directory Authentication Library – Id: Microsoft.IdentityModel.Clients.ActiveDirectory
Json.NET – Id: Newtonsoft.Json

WebConfig Values and parameters in the code

 

<add key=”MSGraphGourps_Realm” value=”yourrealm.com” />

<add key=”ClientId” value=”your client ID in the Azure AD application” />
<add key=”ClientSecret” value=”your client secret key in the Azure AD application” />

The URL format for the authentication context request is something like this: https://login.microsoftonline.com/yourrealm.com

To acquire the token: https://graph.microsoft.com/

To get the groups the URL is: https://graph.microsoft.com/1.0/yourrealm.com/groups

If you are not sure where to get your realm from there are two ways:

  1. Get it from the URL in your browser bar when you are viewing an O365 outlook mail box or group: https://outlook.office.com/owa/?realm=yourrealm.com
  2. The other option is to use the Azure Management UI and again in the url your should find it: https://manage.windowsazure.com/yourrealm.com

 

Getting other information from Groups

One Drive:

https://graph.microsoft.com/v1.0/your realm name/groups/{your group guid}/drive/root

Owners:

https://graph.microsoft.com/v1.0/your realm name/groups/{your group guid}/owners

Group Logo:

 

PnP Partner-Pack Site Provisioning: Weirdness, quirks and guide

If you have been playing around with the Site Provisioning in the PnP Partner Pack you might have run into problems when installing it. While the instructions are rather good there are still some problems which I ran into and are not mentioned in the instruction(or I might have just passed by them).

Notice that the things mentioned here assumes that you have read or understand how to install the site provisioning web app by this instruction:

https://github.com/OfficeDev/PnP-Partner-Pack/blob/master/Documentation/Manual-Setup-Guide.md

Anyway here is what is good to know:

Sign-ON URL format:

When you are defining your Sign-On URL in the Configure section for your application, there is a very weird and annoying error that the site provisioning web application might throw. This is due that you are missing one single character from the end of your URL which is: /

Example:

URL which does not work: https://pnpprovisioning82.azurewebsites.net

URL which does work: https://pnpprovisioning82.azurewebsites.net/

Do you see the difference :)? Small but enough to make you wonder. The error message is not necessarily very informative and unfortunately I do not have that error message anymore, can paste it here. Just check this thing in case of errors.

The webjobs…:

Installation

The web jobs are good and working BUT the installation is a bit misleading. I had to figure out for myself and gather the info from different locations. My problem was that the script which was supposed to install the jobs automatically did not work. Some error which did no make sense and I did not want to spend time figuring out. If it works for you then that is great. If not you might have to do this manually.

You have different way on how to do this manually:

  1. Through Azure portal management
  2. Through visual studio

 

Azure portal management

  1. Build a release version of the webjob
  2. Just zip the entire release folder and rename your zip package
  3. Go to your web application web jobs section
  4. Click add a new job and upload your ZIP file and define how you want your job to run

 

Visual Studio

On the web job project which you want to upload to Azure press the second button and chose  “Publish as Azure Webjob” and follow the instructions.

You might want to take advantage of the Azure management UI by downloading a publishing profile from your web application dashboard. You can use this publishing profile in Visual Studio.

The webjobs what they do

OfficeDevPnP.PartnerPack.ContinousJob

This is the one which will be reading the PnPProvisioningJobs list and create jobs to be processed by the other webjobs. You have to have this on to get anything done, mostly related to site provisioning. I might be wrong on some of the details here but having this on is essential.

OfficeDevPnP.PartnerPack.ScheduledJob

My understanding is that his job is what is provisioning the sites and creating templates of sites.

 

Guide – What is where or how things work

The web application overall UI functionality

As is mentioned in this documentation there are several views(pages) which are used with the site provisioning. Notice that not all of them are available through the web application UI and this in my opinion is confusing.

Some of the functionalities are only available in a actual site:

  1. Creating a new sub site
    1. This is only available when you go to a site, from “site settings” select “site contents”. The PnP Partner Pack overrides the default “New subsite” link. I kind of understand this but was not the first place where I expected it to be.
  2. Saving a site template
    1. This can be found under your site “site settings” in the category “site actions”. There is a JS file which is attached to your PnP sites which add a new link here named as “Save site as Provisioning Template”.

The JS file is located in the infrastructure site in the Library: PnPProvisioningTemplates under the folder: Overrides. Through the XML file in this location the JS file is loaded and the two above mentioned things are performed.

Infrastructural Site Collection

What is mean by this is that this is a “dummy” site collection created by you and then you apply the steps(script) https://github.com/OfficeDev/PnP-Partner-Pack/blob/master/scripts/Provision-InfrastructureSiteArtifacts.ps1 on the site collection. This will be the central hub from where all things are operated from regarding site provisioning.

Good to know libraries

PnPProvisioningTemplates

This is where your site provisioning templates are located(right in the root of the library) and all other PnP site provisioning related configuration data.

PnPProvisioningJobs

This is all of the actions(jobs, requests) for sub site, site collection, site templates etc are stored. Based on these the web jobs will start to operate.

 

SharePoint Seach Query: Include or Exclude empty or null dates

This was a tricky one for me. I had to search and figure out by trial and error what works.

To filter out a datetime managed property you could use definition:

-ExpirationTime:1900-01-01..{Today}

Notice the following important things:

  • the minus sign (-) => is to not include someting that is a certain definition
  • ExpirationTime => will be replaced with your managed property name
  • 1900-01-01..{Today} => is a range, the minimum value is the “empty” or “null” value in a SharePoint datetime field. In other words the lowest possible datetime value which is 1900-01-01.

 

To include “null” or “empty” date time values just remove the minus (-) sign

How to change SharePoint AD group names after the group name has changed in Active Directory

Well this is simple really, The code goes through each site collection, identifies groups coming from AD, then retrieves the same group using the SID value from Active Directory and updaes the SharePoint group name based on the value in the Active Directory.

param (
 [string]$SPSiteFilter = "Your application URL",
 [string]$SPADLoginNamePrefix = "c:0+.w|",
 [bool]$UseSAMAccountName = $true
 )

if ((Get-PSSnapin 'Microsoft.SharePoint.PowerShell' -ErrorAction SilentlyContinue) -eq $null){Add-PSSnapin 'Microsoft.SharePoint.PowerShell'}
 if (Get-Module -ListAvailable -Name ActiveDirectory) {
 Write-Host "ActiveDirectory Module exists"
 
} else {
 Write-Host "ActiveDirectory Module does not exist. This Script requires this module. Please check that the module is installed."
 Write-Host "To install the module through PowerShell you can use the following command:"
 Write-Host "Add-WindowsFeature RSAT-AD-PowerShell"
 return
}
 
 Import-Module ActiveDirectory
$spWebApp = Get-SPWebApplication $SPSiteFilter
foreach($site in $spWebApp.Sites)
 {
 Write-Host "Site: $($site.Url)"
 # Notice that the SPADLoginNamePrefix variable prefix is used to identify a AD group and also to parse the SID value
 $site.rootweb.siteusers | where { $_.IsDomainGroup -and $_.UserLogin.ToString().Contains($SPADLoginNamePrefix) } | %{
 
 
 $dispNameSplit = $_.UserLogin.ToString().Replace($SPADLoginNamePrefix, "")
 
 #Get the Account from AD to retrieve to correct name
 $adAccount = Get-ADGroup -Identity $dispNameSplit
 
 $newSPAccountDisplayName = ""
 #Modify the SharePoint Account DisplayName and Name based on the AD SamAccountName OR the Name 
 if($UseSAMAccountName -eq $true)
 { $newSPAccountDisplayName = $adAccount.SamAccountName }
 else
 { $newSPAccountDisplayName = $adAccount.Name }
 
 if($_.DisplayName -ne $newSPAccountDisplayName -or $_.Name -ne $newSPAccountDisplayName)
 {
 Write-Host "Changed SharePoint AD Group: $($_.DisplayName) to: $($newSPAccountDisplayName)"
 $_.DisplayName = $newSPAccountDisplayName
 $_.Name = $newSPAccountDisplayName
 $_.Update()
 } else
 {
 Write-Host "SharePoint AD Group name: $($_.DisplayName) is the same as in AD : $($newSPAccountDisplayName)"
 }
 $adAccount = $null
 }
 $site.Dispose();
}