Wednesday, July 11, 2018

Decouple from RSS feeds urls using IIS reverse proxy

Suppose that we have web site which shows information from multiple RSS feeds. In this example I will use Sharepoint site which consumes RSS feeds using standard RSS-viewer web part but it also can be site on any other technology:

01

In this case if external provider will decide to change RSS url – connection to your site will be also broken. And if you showed it in many places you will need to go through all of them and fix one by one:

02

In case of Sharepoint this may be real problem because you will need to go through all sub sites, locate all RSS-viewer web parts which consume broken RSS feed, edit page and fix RSS feed in web part properties.

It would be better if our site would be decoupled from external RSS urls via some intermediate reverse proxy: in this case on our site we would use proxy urls instead of real urls and in proxy we would just define mapping between proxy url and real RSS url:

03

In this case if url of one of RSS feeds will be changed we will only need to change appropriate mapping between proxy url and RSS url on proxy level – much simpler than go through the site and fix all places where RSS feed is used.

04

Such RSS reverse proxy can be configured using IIS reverse proxy: we will need Application request routing (AAR) and URL rewrite IIS modules installed. After installing AAR go to proxy settings (IIS manager > Server > Application request routing cache > Server proxy settings) and enable proxy there:

05

After that create new site in IIS (in Sharepoint instead of new site you may also create sub folder under /_layouts virtual folder and use it as proxy url. In this case you will need to define url rewrite rules on this sub folder level i.e. not on the root site level – see below), define its url via binding (suppose that it will be http://myproxy.com) or define port and add the following rewrite rule:

06

Here we tell IIS that all requests which come to http://myproxy.com/example should be rewritten to the real RSS feed url https://example.com/feed (this is not example of really working RSS feed – just for example). Now if external RSS feed’s url https://example.com/feed will be changed – you will only need to go there and change it in the URL rewrite rule. Your site still will use http://myproxy.com/example in all places and won’t require changes.

Monday, July 2, 2018

Sharepoint MVP 2018

As you probably know MS changed MVP award renewal dates and now renewal emails are coming 1th July. I’ve got the following exciting email yesterday: Congratulations 2018-2019 Microsoft MVPs. This is my 8th award and I’m very happy to be part of the great community. In my regular work I try to participate in Sharepoint and Office 365 communities and help people to solve technical problems. Big thanks to MS for recognizing these efforts. In last few years focus has been moved from on-prem Sharepoint to Sharepoint Online, Office 365 and Azure. This is very interesting to observe how these new platforms and services grow and evolve interacting with each other. And even more interesting to take part in this process by contributing to community life and communicating with MS product teams. Thanks to all readers of my blog and see you with new challenges and solutions :)

Thursday, June 14, 2018

Workaround for hanging local web server launched with gulp serve

If you develop Sharepoint Framework web parts (SPFx web parts) you probably familiar with one part of it’s development process – local web server which is launched with gulp:

gulp serve –nobrowser

It launches local web server on localhost:port which allows you to test SPFx web part on own dev environment (not release version) which consumes js and css files directly from this localhost:port address. So you may modify them and reload the page without redeploying app package to App catalog (which is time consuming if you develop js/css).

However sometimes local web server hangs without visible reasons. In order to avoid it you may stop it (Ctrl-C) and run again – but it also takes time and not always help. If you faced with this problem go to cmd window with launched gulp serve and try click Esc several times. If after that it will start to show output like this:

Request: ‘/dist/mywebpart-bundle.js’
Request: ‘…’
Request: ‘…’

it will mean that server unfreezes and should work again now.

Friday, June 8, 2018

Avoid error “Invalid property 'responseHeaders'” when try to update Azure AD group using Graph client library

Some time ago we faced with interesting problem: when tried to update group using UnifiedGroupsUtility.UpdateUnifiedGroup() method from OfficeDevPnP we got the following error:

HTTP/1.1 400 Bad Request

ef
{
  "error": {
    "code": "Request_BadRequest",
    "message": "Invalid property 'responseHeaders'.",
    "innerError": {
      "request-id": "...",
      "date": "..."
    }
  }
}

Under the hood this method uses .Net Graph client library:

var graphClient = CreateGraphClient(accessToken, retryCount, delay);

var groupToUpdate = await graphClient.Groups[groupId]
	.Request()
	.GetAsync();

if (!String.IsNullOrEmpty(description) && groupToUpdate.Description != description)
{
	groupToUpdate.Description = description;
	updateGroup = true;
}

bool existingIsPrivate = groupToUpdate.Visibility == "Private";
if (existingIsPrivate != isPrivate)
{
	groupToUpdate.Visibility = isPrivate == true ? "Private" : "Public";
	updateGroup = true;
}

if (updateGroup)
{
	var updatedGroup = await graphClient.Groups[groupId]
		.Request()
		.UpdateAsync(groupToUpdate);
}

When I checked in Fiddler request details of UpdateAsync() call I found that JSON representation of group object which is passed to HTTP PATCH method really has responseHeaders property:

PATCH https://graph.microsoft.com/v1.0/groups/{id}

{
  "classification": "Internal",
  "createdDateTime": "...",
  "description": "test",
  "displayName": "...",
  "groupTypes": [
    "Unified"
  ],
  "mail": "...",
  "mailEnabled": true,
  "mailNickname": "...",
  "proxyAddresses": [
    "SMTP:..."
  ],
  "renewedDateTime": "...",
  "securityEnabled": false,
  "visibility": "Public",
  "id": "...",
  "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#groups/$entity",
  "deletedDateTime": null,
  "onPremisesLastSyncDateTime": null,
  "onPremisesProvisioningErrors": [],
  "onPremisesSecurityIdentifier": null,
  "onPremisesSyncEnabled": null,
  "preferredDataLocation": null,
  "resourceBehaviorOptions": [],
  "resourceProvisioningOptions": [],
  "responseHeaders": {
    "Transfer-Encoding": [
      "chunked"
    ],
    "request-id": [
      "..."
    ],
    "client-request-id": [
      "..."
    ],
    "x-ms-ags-diagnostic": [
      ...
    ],
    "OData-Version": [
      "4.0"
    ],
    "Duration": [
      "100.3015"
    ],
    "Strict-Transport-Security": [
      "max-age=31536000"
    ],
    "Cache-Control": [
      "private"
    ],
    "Date": [
      "..."
    ]
  },
  "statusCode": "OK"
}

Not sure why responseHeaders property is now added to the group object when it is first returned from graph. In order to avoid this error I used the following workaround: construct new Group object, specify group id and only those properties which should be updated. I.e. update object with minimal required specified properties:

var graphClient = CreateGraphClient(accessToken);
var existingGroup = await graphClient.Groups[groupId].Request().GetAsync();

var groupToUpdateMinimal = new Group();
groupToUpdateMinimal.Id = groupId;

bool updateGroup = false;
if (!string.IsNullOrEmpty(description) && existingGroup.Description != description)
{
	groupToUpdateMinimal.Description = description;
	updateGroup = true;
}

bool existingIsPrivate = existingGroup.Visibility == "Private";
if (isPrivate != null && existingIsPrivate != isPrivate.Value)
{
	groupToUpdateMinimal.Visibility = isPrivate.Value ? "Private" : "Public";
	updateGroup = true;
}

if (updateGroup)
{
	await graphClient.Groups[groupId].Request().UpdateAsync(groupToUpdateMinimal);
}

In this case only id, description and visibility properties are passed to HTTP PATCH method:

{
  "description": "test",
  "visibility": "Public",
  "id": "..."
}

and group is successfully updated.

Wednesday, May 30, 2018

How to hide Shared With ECB menu item for non-admin users via javascript

In Sharepoint lists and doclib there is possibility to check which users have access to list item or file using “Shared With”. Often this item should be hidden for non-admin users. Below javascript allows to hide “Shared With” ECB menu item for users which are not site admins:

SP.SOD.executeFunc("sp.js", "SP.ClientContext", function () {
	var ctx = SP.ClientContext.get_current();
	var user = ctx.get_web().get_currentUser();
	ctx.load(user);
	ctx.executeQueryAsync(
		Function.createDelegate(this, function () {
			var isSiteAdmin = user.get_isSiteAdmin();
			if (isSiteAdmin) {
				return;
			}
			jQuery("td.ms-list-itemLink-td").click(function(){
				setTimeout(function(){
					jQuery("span.js-callout-ecbActionDownArrow a").click(function(){
						setTimeout(function(){
							jQuery("li[text='Shared With']").hide();
						}, 200);
					});
				}, 200);
			});
		}),
		Function.createDelegate(this, function (sender, args) {
			// log error
		}));
});

Here we first check whether current user site admin or not. After that we add our own event handler for ECB click item which hides “Shared With” command. Little delay is needed in order to allow ECB sub menu to be displayed after original click handler will be triggered.

Monday, May 28, 2018

Set O365 group classification via Graph API

Some time ago MS announced possibility to specify groups classifications – see e.g. Classifications for Office365 Groups and Microsoft Teams. After you configured classifications on tenant level like shown in mentioned article when you go to Sharepoint app from your Office 365 App launcher (it will lead to https://{tenant}.sharepoint.com/_layouts/15/sharepoint.aspx) and choose “+ Create site” from the header – you will see dropdown list with specified classifications:

2018-05-28_17-14-43

When you will create new site classification will be shown in the header:

2018-05-28_17-16-26

Also if you will get related group via Graph API – classification will be returned with other group’s properties:

GET https://graph.microsoft.com/v1.0/groups/{id}/classification

will return

{
  "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#groups('{id}')/classification",
  "value": "internal"
}

But is it possible to update group’s classification programmatically? If we just try to send PACTH request on https://graph.microsoft.com/v1.0/groups/{id} endpoint we will get the following error:

{
    "error": {
        "code": "Request_BadRequest",
        "message": "Property 'classification' is read-only and cannot be set.",
        "innerError": {
            "request-id": "...",
            "date": "2018-05-28T14:22:55"
        }
    }
}

The answer is however yes it is possible: in order to set classification of O365 group via Graph API you need to use beta endpoint instead of v1.0: https://graph.microsoft.com/beta/groups/{id}. In this case property should be successfully updated.

Note that team site classification is stored in own property Site.Classification. So if you updated group’s classification you may also want to update classification of related site. If you use SiteExtension.SetSiteClassification from OfficeDevPnP both actions will be done automatically.

Friday, May 25, 2018

Solve problem with missing Get-AzureADDirectorySetting and Set-AzureADDirectorySetting PowerShell cmdlets

If you want to play with Office 365 classifications you will need to configure tenant-level directory settings which contains available values which can be used for groups’ classifications. You may do it using Get-AzureADDirectorySetting and Set-AzureADDirectorySetting cmdlets like shown in the following article: Classifications for Office365 Groups and Microsoft Teams. But if you will follow this article and install both AzureAD and AzureADPreview modules from PowerShell gallery:

Install-Module AzureAD
Install-Module AzureADPreview

and then will connect to Azure AD using command:

Connect-AzureAD

and after that will try to use Get-AzureADDirectorySetting cmdlet you may get the following error:

The term 'Get-AzureADDirectorySetting' is not recognized as the name of a cmdlet,function, script file, or operable program

The problem is that on the moment of writing this post Get-AzureADDirectorySetting cmdlet was defined only in AzureADPreview module. You may check it if will execute the following command:

man Get-AzureADDirectory*

For me it produced the following result:

Name                              Category  Module                    Synopsis
----                              --------  ------                    --------
Get-AzureADDirectoryRole          Cmdlet    AzureADPreview            Gets a directory role.
Get-AzureADDirectoryRoleMember    Cmdlet    AzureADPreview            Gets members of a directory role.
Get-AzureADDirectoryRoleTemplate  Cmdlet    AzureADPreview            Gets directory role templates.
Get-AzureADDirectorySetting       Cmdlet    AzureADPreview            Gets a directory setting.
Get-AzureADDirectorySettingTem... Cmdlet    AzureADPreview            Gets a directory setting template.
Get-AzureADDirectoryRoleMember    Cmdlet    AzureAD                   Get-AzureADDirectoryRoleMember...
Get-AzureADDirectoryRole          Cmdlet    AzureAD                   Get-AzureADDirectoryRole...
Get-AzureADDirectoryRoleTemplate  Cmdlet    AzureAD                   Get-AzureADDirectoryRoleTemplate..

As you can see Get-AzureADDirectorySetting cmdlet is defined in AzureADPreview.

So solution is to start new PowerShell session and connect to Azure AD using Connect-AzureAD from AzureADPreview module:

AzureADPreview\Connect-AzureAD

After that Get-AzureADDirectorySetting and Set-AzureADDirectorySetting cmdlets should become available.