Wednesday, May 16, 2018

Redirect Sharepoint WebDAV network locations from http to https

As you probably know it is possible to connect to Sharepoint sites and doclibs via WebDAV in Wnidows explorer view, by creating new network location or by mapping new network drive to Sharepoint site. If your site works via http WebDAV will use http, if site uses https – WebDAV also will work over https. What if you want to force users to use https instead of http?

First of all you have to configure your Sharepoint stie to use https (purchase SSL certificate, install it on your IIS server, add https binding to Sharepoint site in IIS manager, change alternate access mappings in Central administration). After that you need to redirect traffic from http to https. In order to do that use URL Rewrite IIS module and add new blank rule which looks like this:

2018-05-16_11-27-55

This rule will redirect http requests to https in browser. But also it will redirect http to https for WebDAV clients. If you will open fiddler and will try to open network location which uses http url then you will see something like that (test was done on Windows 10 client):

Request headers:

PROPFIND http://example.com
User-Agent: Microsoft-WebDAV-MiniRedir/10.0.16299

Response headers:

HTTP/1.1 301 Moved Permanently
Location: https://example.com

Where instead of example.com will be url of your Sharepoint site. I.e. URL Rewrite IIS module successfully redirects traffic also for WebDAV.

Tuesday, May 8, 2018

Problem with sync delay between Azure AD and Sharepoint Online when Rest API is used

When you create user or group in Azure AD it is not immediately available in Sharepoint Online. I wrote about this problem here: Problem with delayed propagation of Azure AD groups to Sharepoint Online. In this post I will describe another interesting problem which may occur because of this delay.

Azure AD group members and owners may be retrieved with Graph API and with Rest API:

Graph AP endpoint:

https://graph.microsoft.com/v1.0/groups/{groupId}/members

Rest API endpoint

http://example.com/_api/SP.Directory.DirectorySession/Group('{groupId}')/members?$select=displayName,id

where instead of http://example.com you need to use url of your Sharepoint site.

The problem is that until Azure AD data won’t be fully synced to Sharepoint Online Rest API may return not correct data. E.g. /members endpoint may return actually owners, while /owners endpoint may not return users at all. Depending on how fast MS data center will propagate changes it may take up to several hours. So be aware about this problem.

The main advantage of Rest API endpoint is that it returns members count. While in Gtaph API $count query string parameter is not supported for users and groups: Use query parameters to customize responses:

Note: $count is not supported for collections of resources that derive from directoryObject like collections of users or groups.

So you may want to use Rest but notice that it may work incorrectly first several hours.

Friday, April 20, 2018

List Azure AD groups via CSOM in Powershell

Recently I showed how to list Azure AD/O365 groups in Powershell using REST Graph API: see List Azure AD groups via Rest Graph API in Powershell. In this post I will show how to do the same thing using CSOM. It may be interesting especially in part of how to obtain access token based on client id and client secret via CSOM and how it differs from REST approach.

In order to fully understand the code you may also need to check another article: Avoiding StackOverflowException when use assembly binding redirect in PowerShell. It shows how to use C#-based assembly binding redirect in PowerShell (native Powershell assembly binding redirect which is described here Use specific version of Sharepoint client object model in PowerShell via assembly binding redirection causes StackOverflowException when you try to use Graph API).

So here is the script:

param
(
	[Parameter(Mandatory=$true)]
	$Tenant,
	[Parameter(Mandatory=$true)]
	$ClientId,
	[Parameter(Mandatory=$true)]
	$ClientSecret
)

$currentDir = Convert-Path(Get-Location)
$dllCommonDir = resolve-path($currentDir + "\..\Assemblies\Common\")
$pnpCoreDir = resolve-path($currentDir + "\..\packages\SharePointPnPCoreOnline.2.22.1801.0\lib\net45\")
$graphDir = resolve-path($currentDir + "\..\packages\Microsoft.Graph.1.7.0\lib\net45\")
$graphCoreDir = resolve-path($currentDir + "\..\packages\Microsoft.Graph.Core.1.7.0\lib\net45\")
$newtonJsonDir = resolve-path($currentDir + "\..\packages\Newtonsoft.Json.10.0.3\lib\net45\")

$AssemblyNewtonJson = [System.Reflection.Assembly]::LoadFile([System.IO.Path]::Combine($newtonJsonDir, "Newtonsoft.Json.dll"))
$AssemblyGraphCore = [System.Reflection.Assembly]::LoadFile([System.IO.Path]::Combine($graphCoreDir, "Microsoft.Graph.Core.dll"))
$AssemblyGraph = [System.Reflection.Assembly]::LoadFile([System.IO.Path]::Combine($graphDir, "Microsoft.Graph.dll"))
[System.Reflection.Assembly]::LoadFile([System.IO.Path]::Combine($dllCommonDir, "Microsoft.Identity.Client.dll"))
[System.Reflection.Assembly]::LoadFile([System.IO.Path]::Combine($pnpCoreDir, "OfficeDevPnP.Core.dll"))

if (!("Redirector" -as [type]))
{
$source = 
@'
using System;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;

public class Redirector
{
    public readonly string[] ExcludeList;

    public Redirector(string[] ExcludeList = null)
    {
        this.ExcludeList  = ExcludeList;
        this.EventHandler = new ResolveEventHandler(AssemblyResolve);
    }

    public readonly ResolveEventHandler EventHandler;

    protected Assembly AssemblyResolve(object sender, ResolveEventArgs resolveEventArgs)
    {
        //Console.WriteLine("Attempting to resolve: " + resolveEventArgs.Name); // remove this after its verified to work
        foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
        {
            var pattern  = "PublicKeyToken=(.*)$";
            var info     = assembly.GetName();
            var included = ExcludeList == null || !ExcludeList.Contains(resolveEventArgs.Name.Split(',')[0], StringComparer.InvariantCultureIgnoreCase);

            if (included && resolveEventArgs.Name.StartsWith(info.Name, StringComparison.InvariantCultureIgnoreCase))
            {
                if (Regex.IsMatch(info.FullName, pattern))
                {
                    var Matches        = Regex.Matches(info.FullName, pattern);
                    var publicKeyToken = Matches[0].Groups[1];

                    if (resolveEventArgs.Name.EndsWith("PublicKeyToken=" + publicKeyToken, StringComparison.InvariantCultureIgnoreCase))
                    {
                        //Console.WriteLine("Redirecting lib to: " + info.FullName); // remove this after its verified to work
                        return assembly;
                    }
                }
            }
        }

        return null;
    }
}
'@

    $type = Add-Type -TypeDefinition $source -PassThru 
}

try
{
    $redirector = [Redirector]::new($null)
    [System.AppDomain]::CurrentDomain.add_AssemblyResolve($redirector.EventHandler)
}
catch
{
    #.net core uses a different redirect method
    Write-Warning "Unable to register assembly redirect(s). Are you on ARM (.Net Core)?"
}

function GetAccessToken($tenant, $clientId, $clientSecret)
{
	$appCredentials = New-Object Microsoft.Identity.Client.ClientCredential -ArgumentList $clientSecret
	$aadLoginUri = New-Object System.Uri -ArgumentList "https://login.microsoftonline.com/"
	$authorityUri = New-Object System.Uri -ArgumentList $aadLoginUri, $tenant
	$authority = $authorityUri.AbsoluteUri
	$redirectUri = "urn:ietf:wg:oauth:2.0:oob"
	$clientApplication = New-Object Microsoft.Identity.Client.ConfidentialClientApplication($clientId, $authority, $redirectUri, $appCredentials, $null, $null)
	[string[]]$defaultScope = @("https://graph.microsoft.com/.default")
	$authenticationResult = $clientApplication.AcquireTokenForClientAsync($defaultScope).Result
	return $authenticationResult.AccessToken
}

function ListGroups($accessToken)
{
	$existingGroups = [OfficeDevPnP.Core.Framework.Graph.UnifiedGroupsUtility]::ListUnifiedGroups($accessToken, "", "")
	foreach($group in $existingGroups)
	{
		Write-Host $group.DisplayName
	}
}

$accessToken = GetAccessToken $Tenant $ClientId $ClientSecret
ListGroups $accessToken

Tuesday, April 17, 2018

List Azure AD groups via Rest Graph API in Powershell

The following PowerShell snipped shows how to acquire acces token based on client id/client secret via REST Graph API and list all Azure AD groups in Powershell:

param
(
	[Parameter(Mandatory=$true)]
	[string]$Tenant,
	[Parameter(Mandatory=$true)]
	[string]$ClientId,
	[Parameter(Mandatory=$true)]
	[string]$ClientSecret
)

$currentDir = [System.IO.Directory]::GetCurrentDirectory()
$dllCommonDir = resolve-path($currentDir + "\..\..\Assemblies\Common\")
[System.Reflection.Assembly]::LoadFile([System.IO.Path]::Combine($dllCommonDir, "Microsoft.Identity.Client.dll"))

function GetAccessToken($tenant, $clientId, $clientSecret)
{
	$appCredentials = New-Object Microsoft.Identity.Client.ClientCredential -ArgumentList $clientSecret
	$aadLoginUri = New-Object System.Uri -ArgumentList "https://login.microsoftonline.com/"
	$authorityUri = New-Object System.Uri -ArgumentList $aadLoginUri, $tenant
	$authority = $authorityUri.AbsoluteUri
	$redirectUri = "urn:ietf:wg:oauth:2.0:oob"
	$clientApplication = New-Object Microsoft.Identity.Client.ConfidentialClientApplication($clientId, $authority, $redirectUri, $appCredentials, $null, $null)
	[string[]]$defaultScope = @("https://graph.microsoft.com/.default")
	$authenticationResult = $clientApplication.AcquireTokenForClientAsync($defaultScope).Result
	return $authenticationResult.AccessToken
}

function RetrieveGroupsRest($accessToken)
{
	$authHeader = @{
		"Content-Type"="application\json"
		"Authorization"="Bearer " + $accessToken
		}

	$uri = "https://graph.microsoft.com/v1.0/groups"
    $result = @()
    do{
        $objects = Invoke-RestMethod -Uri $uri -Headers $authHeader -Method Get
        $uri = $objects.'@odata.nextlink'
        $result = $result + $objects.value
       
    }until ($uri -eq $null)
	return $result
}

$accessToken = GetAccessToken $Tenant $ClientId $ClientSecret
$dataFromGraphAPI = RetrieveGroupsRest $accessToken
$dataFromGraphAPI | ft -Property id,displayName

Thursday, April 5, 2018

Fix error Failed to call GetTypes on assembly Microsoft.Office.TranslationServices after installing Sharepoint 2013 March 2013 CU

After installing Sharepoint 2013 March 2013 CU (and higher) you may get the following error when try to do something in the site:

Failed to call GetTypes on assembly Microsoft.Office.TranslationServices, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c. Method not found

In order to fix it try the following steps:

1. On your Sharepoint server go to the GAC and find folder of Microsoft.Office.TranslationServices.dll assembly (e.g. C:\Windows\Microsoft.NET\assembly\GAC_MSIL\Microsoft.Office.TranslationServices\v4.0_15.0.0.0__71e9bce111e9429c)

2. Open folder in Explorer > right click on Microsoft.Office.TranslationServices.dll > Properties > Details and check actual assembly version:

2018-04-05_10-16-33

in this example full version is 15.0.4420.1017.

3. Go to virtual folder of your Sharepoint site (C:\inetpub\wwwroot\wss\VirtualDirectories\{Site})

4. Edit web.config > find <runtime>/<assemblyBinding> section and add the following section to the end:

<dependentAssembly xmlns="urn:schemas-microsoft-com:asm.v1">
  <assemblyIdentity name="Microsoft.Office.TranslationServices" publicKeyToken="71e9bce111e9429c" culture="neutral" />
  <bindingRedirect oldVersion="1.0.0.0-15.0.0.0" newVersion="15.0.4420.1017" />
</dependentAssembly>
<dependentAssembly xmlns="urn:schemas-microsoft-com:asm.v1">
  <assemblyIdentity name="Microsoft.Office.TranslationServices.ServerStub" publicKeyToken="71e9bce111e9429c" culture="neutral" />
  <bindingRedirect oldVersion="1.0.0.0-15.0.0.0" newVersion="15.0.4420.1017" />
</dependentAssembly>

Note that it is important to specify both assemblies - if you will only specify Microsoft.Office.TranslationServices you will still get same error when SharePoint will try to call /_vti_bin/client.svc (which in turn may cause other unclear problems like error "The server was unable to save the form at this time. Please try again" when you try to create or edit list item). After that error should disappear. If similar error will occur for different assembly try to add assembly bindnig redirection for it as well.

Wednesday, March 28, 2018

Avoid problem with redirecting Sharepoint sites which use .dev sub domains to https in Chrome and Firefox

As you probably heard some time ago Chrome and FF started to redirect sites which use .dev sub domains in URL to https. Issue is described e.g. here: Chrome & Firefox now force .dev domains to HTTPS via preloaded HSTS. Briefly .dev domain is owned by Google and they added HSTS rule for it which forces redirection to https. You may check it in Chrome: open chrome://net-internals#hsts and make query for dev domain:

2018-03-28_16-38-32

As you can see there is FORCE_HTTPS rule.

This problem may occur in local dev Sharepoint environments which often use .dev domain. So if you have e.g. site http://intranet.sp.dev and will try to open it in Chrome or Firefox it won’t open it with unclear error:

2018-03-28_16-37-56

But if you notice url address bar of the browser you will find that it actually tries to open https://intranet.sp.dev instead of http://intranet.sp.dev and it causes issue. You may try to add https binding to the site using self-signed certificate, but browser will still show error that connection is not secure.

The following solution may be used for avoiding this problem with Sharepoint site. It requires using new site address http://intranet.sp.local for your Sharepoint site instead of http://intranet.sp.dev:

  • add intranet.sp.local binding for your Sharepoint site in IIS Manager
  • in C:/Windows/System32/drivers/etc/host add
      127.0.0.1 intranet.sp.local
  • in Central administration > Alternate access mappings > Add internal url for not used zone: http://intranet.sp.local

After that site can be opened in Chrome and FF using http://intranet.sp.local url.

Monday, March 26, 2018

Avoiding StackOverflowException when use assembly binding redirect in PowerShell

Recently I faced with interesting problem with assembly binding redirect in PowerShell: in my script I needed to use the following versions of the assemblies (these exact versions were used in other components and I couldn’t add another versions because of number of reasons):

OfficeDevPnP.Core 2.22.1801.0
Microsoft.Graph 1.7.0.0
Microsoft.Graph.Core 1.7.0.0

The problem is that OfficeDevPnP.Core 2.22.1801.0 references different versions of Microsoft.Graph and Microsoft.Grap.Core:

Microsoft.Graph 1.1.1.0
Microsoft.Graph.Core 1.2.1.0

So I needed to use assembly binding redirect. At first I tried approach described in the following post: Use specific version of Sharepoint client object model in PowerShell via assembly binding redirection:

$currentDir = Convert-Path(Get-Location)
$pnpCoreDir = resolve-path($currentDir + "\..\packages\SharePointPnPCoreOnline.2.22.1801.0\lib\net45\")
$graphDir = resolve-path($currentDir + "\..\packages\Microsoft.Graph.1.7.0\lib\net45\")
$graphCoreDir = resolve-path($currentDir + "\..\packages\Microsoft.Graph.Core.1.7.0\lib\net45\")

$AssemblyGraphCore = [System.Reflection.Assembly]::LoadFile([System.IO.Path]::Combine($graphCoreDir, "Microsoft.Graph.Core.dll"))
$AssemblyGraph = [System.Reflection.Assembly]::LoadFile([System.IO.Path]::Combine($graphDir, "Microsoft.Graph.dll"))
[System.Reflection.Assembly]::LoadFile([System.IO.Path]::Combine($pnpCoreDir, "OfficeDevPnP.Core.dll"))


<#$OnAssemblyResolve = [System.ResolveEventHandler] {
	param($sender, $e)
	
	if ($e.Name.StartsWith("Microsoft.Graph,"))
	{
		$AssemblyGraph = [System.Reflection.Assembly]::LoadFile([System.IO.Path]::Combine($graphDir, "Microsoft.Graph.dll"))
		return $AssemblyGraph
	}
	if ($e.Name.StartsWith("Microsoft.Graph.Core,"))
	{
		$AssemblyGraphCore = [System.Reflection.Assembly]::LoadFile([System.IO.Path]::Combine($graphCoreDir, "Microsoft.Graph.Core.dll"))
		return $AssemblyGraphCore
	}

	foreach($a in [System.AppDomain]::CurrentDomain.GetAssemblies())
	{
		if ($a.FullName -eq $e.Name)
		{
		  return $a
		}
	}
	Write-Host "Return null" -foregroundcolor red
	return $null
}
[System.AppDomain]::CurrentDomain.add_AssemblyResolve($OnAssemblyResolve)

However attempt to call method from loaded assemblies caused StackOverflowException and closing PowerShell session.

After that I tried C#-based assembly redirector (found it in the following forum thread: Powershell - Assembly binding redirect NOT found in application configuration file):

if (!("Redirector" -as [type]))
{
$source = 
@'
using System;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;

public class Redirector
{
    public readonly string[] ExcludeList;

    public Redirector(string[] ExcludeList = null)
    {
        this.ExcludeList  = ExcludeList;
        this.EventHandler = new ResolveEventHandler(AssemblyResolve);
    }

    public readonly ResolveEventHandler EventHandler;

    protected Assembly AssemblyResolve(object sender, ResolveEventArgs resolveEventArgs)
    {
        Console.WriteLine("Attempting to resolve: " + resolveEventArgs.Name); // remove this after its verified to work
        foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
        {
            var pattern  = "PublicKeyToken=(.*)$";
            var info     = assembly.GetName();
            var included = ExcludeList == null || !ExcludeList.Contains(resolveEventArgs.Name.Split(',')[0], StringComparer.InvariantCultureIgnoreCase);

            if (included && resolveEventArgs.Name.StartsWith(info.Name, StringComparison.InvariantCultureIgnoreCase))
            {
                if (Regex.IsMatch(info.FullName, pattern))
                {
                    var Matches        = Regex.Matches(info.FullName, pattern);
                    var publicKeyToken = Matches[0].Groups[1];

                    if (resolveEventArgs.Name.EndsWith("PublicKeyToken=" + publicKeyToken, StringComparison.InvariantCultureIgnoreCase))
                    {
                        Console.WriteLine("Redirecting lib to: " + info.FullName); // remove this after its verified to work
                        return assembly;
                    }
                }
            }
        }

        return null;
    }
}
'@

    $type = Add-Type -TypeDefinition $source -PassThru 
}

try
{
    $redirector = [Redirector]::new($null)
    [System.AppDomain]::CurrentDomain.add_AssemblyResolve($redirector.EventHandler)
}
catch
{
    #.net core uses a different redirect method
    Write-Warning "Unable to register assembly redirect(s). Are you on ARM (.Net Core)?"
}

And surprisingly this approach worked. It looks like a bug in PowerShell script-based assembly binding redirect. Hope it will help someone.